Add free GAS for Vita holders with cross-chain fee splitting

Rename PersonToken to Vita (soul-bound identity token) and implement
fee exemption system for citizens:

- Add Federation native contract for cross-chain Vita coordination
  - Visitor registry for Vita holders from other chains
  - Configurable visiting fee percentage (default 50%)
  - Inter-chain debt tracking for settlement between chains

- Modify GAS contract to burn fees from Treasury for Vita holders
  - Local Vita: 100% paid by local Treasury
  - Visiting Vita: Split between local Treasury and inter-chain debt
  - Deficit tracking when Treasury is underfunded

- Update mempool and blockchain to skip fee checks for Vita exempt users
  - Add IsVitaFeeExempt() to Feer interface
  - Modify verifyAndPoolTx() to allow zero-fee transactions

- Rename PersonToken -> Vita across codebase
  - Update state, native contract, and tests
  - Maintain same functionality with clearer naming

🤖 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 04:27:55 +00:00
parent b5c1dca2c6
commit f03564d676
20 changed files with 1108 additions and 412 deletions

View File

@ -218,6 +218,9 @@ type Blockchain struct {
designate native.IDesignate
oracle native.IOracle
notary native.INotary
vita native.IVita
federation native.IFederation
treasury native.ITreasury
extensible atomic.Value
@ -465,6 +468,18 @@ func NewBlockchain(s storage.Store, cfg config.Blockchain, log *zap.Logger, newN
return nil, err
}
}
bc.vita = bc.contracts.Vita()
if err := validateNative(bc.vita, nativeids.Vita, nativenames.Vita, nativehashes.Vita); err != nil {
return nil, err
}
bc.federation = bc.contracts.Federation()
if err := validateNative(bc.federation, nativeids.Federation, nativenames.Federation, nativehashes.Federation); err != nil {
return nil, err
}
bc.treasury = bc.contracts.Treasury()
if err := validateNative(bc.treasury, nativeids.Treasury, nativenames.Treasury, nativehashes.Treasury); err != nil {
return nil, err
}
bc.persistCond = sync.NewCond(&bc.lock)
bc.gcBlockTimes, _ = lru.New[uint32, uint64](defaultBlockTimesCache) // Never errors for positive size
@ -2442,6 +2457,26 @@ func (bc *Blockchain) GetUtilityTokenBalance(acc util.Uint160) *big.Int {
return bs
}
// IsVitaFeeExempt returns true if the account has an active Vita token
// (local or visiting) and is exempt from paying transaction fees.
// This implements the Feer interface.
func (bc *Blockchain) IsVitaFeeExempt(acc util.Uint160) bool {
// Check local Vita first
if bc.vita != nil {
token, err := bc.vita.GetTokenByOwner(bc.dao, acc)
if err == nil && token != nil && token.Status == state.TokenStatusActive {
return true
}
}
// Check visiting Vita registry
if bc.federation != nil {
if bc.federation.IsVisitor(bc.dao, acc) {
return true
}
}
return false
}
// GetGoverningTokenBalance returns governing token (NEO) balance and the height
// of the last balance change for the account.
func (bc *Blockchain) GetGoverningTokenBalance(acc util.Uint160) (*big.Int, uint32) {
@ -2946,7 +2981,8 @@ func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool.
}
needNetworkFee := int64(size)*bc.FeePerByte() + bc.CalculateAttributesFee(t)
netFee := t.NetworkFee - needNetworkFee
if netFee < 0 {
// Skip fee check for Vita holders (local or visiting) - their fees are paid by Treasury
if netFee < 0 && !bc.IsVitaFeeExempt(t.Sender()) {
return fmt.Errorf("%w: net fee is %v, need %v", ErrTxSmallNetworkFee, t.NetworkFee, needNetworkFee)
}
// check that current tx wasn't included in the conflicts attributes of some other transaction which is already in the chain

View File

@ -11,4 +11,7 @@ type Feer interface {
FeePerByte() int64
GetUtilityTokenBalance(util.Uint160) *big.Int
BlockHeight() uint32
// IsVitaFeeExempt returns true if the account has an active Vita token
// (local or visiting) and is exempt from paying transaction fees.
IsVitaFeeExempt(util.Uint160) bool
}

View File

@ -164,6 +164,13 @@ func (mp *Pool) HasConflicts(t *transaction.Transaction, fee Feer) bool {
// and returns false if both balance check is required and the sender does not have enough GAS to pay.
func (mp *Pool) tryAddSendersFee(tx *transaction.Transaction, feer Feer, needCheck bool) bool {
payer := tx.Signers[mp.payerIndex].Account
// Skip balance check for Vita fee exempt users (local or visiting Vita holders).
// Their fees are paid by the Treasury.
if feer.IsVitaFeeExempt(payer) {
return true
}
senderFee, ok := mp.fees[payer]
if !ok {
_ = senderFee.balance.SetFromBig(feer.GetUtilityTokenBalance(payer))

View File

@ -40,6 +40,10 @@ func (fs *FeerStub) GetUtilityTokenBalance(uint160 util.Uint160) *big.Int {
return big.NewInt(fs.balance)
}
func (fs *FeerStub) IsVitaFeeExempt(util.Uint160) bool {
return false
}
func testMemPoolAddRemoveWithFeer(t *testing.T, fs Feer) {
mp := New(10, 0, false, nil)
tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0)

View File

@ -111,12 +111,12 @@ type (
GetMaxNotValidBeforeDelta(dao *dao.Simple) uint32
}
// IPersonToken is an interface required from native PersonToken contract
// IVita is an interface required from native Vita contract
// for interaction with Blockchain and other native contracts.
IPersonToken interface {
IVita interface {
interop.Contract
GetTokenByOwner(d *dao.Simple, owner util.Uint160) (*state.PersonToken, error)
GetTokenByIDPublic(d *dao.Simple, tokenID uint64) (*state.PersonToken, error)
GetTokenByOwner(d *dao.Simple, owner util.Uint160) (*state.Vita, error)
GetTokenByIDPublic(d *dao.Simple, tokenID uint64) (*state.Vita, error)
TokenExists(d *dao.Simple, owner util.Uint160) bool
GetAttribute(d *dao.Simple, tokenID uint64, key string) (*state.Attribute, error)
// IsAdultVerified checks if the owner has a verified "age_verified" attribute
@ -156,6 +156,37 @@ type (
// IsVendor returns true if address is a registered active vendor.
IsVendor(d *dao.Simple, addr util.Uint160) bool
}
// IFederation is an interface required from native Federation contract for
// interaction with Blockchain and other native contracts.
// Federation manages cross-chain Vita coordination and visiting fee governance.
IFederation interface {
interop.Contract
// GetVisitingFeePercent returns the percentage of fees the host chain pays for visitors.
GetVisitingFeePercent(d *dao.Simple) uint8
// IsVisitor checks if an address is a registered visitor from another chain.
IsVisitor(d *dao.Simple, owner util.Uint160) bool
// GetHomeChain returns the home chain ID for a visitor.
GetHomeChain(d *dao.Simple, owner util.Uint160) uint32
// AddInterChainDebt adds to the inter-chain debt owed to a specific chain.
AddInterChainDebt(d *dao.Simple, chainID uint32, amount *big.Int)
// GetInterChainDebt returns the inter-chain debt owed to a specific chain.
GetInterChainDebt(d *dao.Simple, chainID uint32) *big.Int
// Address returns the contract's script hash.
Address() util.Uint160
}
// ITreasury is an interface required from native Treasury contract for
// interaction with Blockchain and other native contracts.
ITreasury interface {
interop.Contract
// Address returns the contract's script hash.
Address() util.Uint160
// AddDeficit adds to the cumulative deficit tracking.
AddDeficit(d *dao.Simple, amount *big.Int)
// GetDeficit returns the current cumulative deficit.
GetDeficit(d *dao.Simple) *big.Int
}
)
// Contracts is a convenient wrapper around an arbitrary set of native contracts
@ -271,10 +302,10 @@ func (cs *Contracts) Notary() INotary {
return nil
}
// PersonToken returns native IPersonToken contract implementation. It panics if
// Vita returns native IVita contract implementation. It panics if
// there's no contract with proper name in cs.
func (cs *Contracts) PersonToken() IPersonToken {
return cs.ByName(nativenames.PersonToken).(IPersonToken)
func (cs *Contracts) Vita() IVita {
return cs.ByName(nativenames.Vita).(IVita)
}
// RoleRegistry returns native IRoleRegistry contract implementation. It panics if
@ -289,6 +320,18 @@ func (cs *Contracts) VTS() IVTS {
return cs.ByName(nativenames.VTS).(IVTS)
}
// Federation returns native IFederation contract implementation. It panics if
// there's no contract with proper name in cs.
func (cs *Contracts) Federation() IFederation {
return cs.ByName(nativenames.Federation).(IFederation)
}
// Treasury returns native ITreasury contract implementation. It panics if
// there's no contract with proper name in cs.
func (cs *Contracts) Treasury() ITreasury {
return cs.ByName(nativenames.Treasury).(ITreasury)
}
// NewDefaultContracts returns a new set of default native contracts.
func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
mgmt := NewManagement()
@ -325,8 +368,8 @@ func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
treasury := newTreasury()
treasury.NEO = neo
personToken := newPersonToken()
personToken.NEO = neo
vita := newVita()
vita.NEO = neo
// Parse TutusCommittee addresses from config
var tutusCommittee []util.Uint160
@ -344,14 +387,23 @@ func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
roleRegistry := newRoleRegistry(tutusCommittee)
roleRegistry.NEO = neo
// Set RoleRegistry on PersonToken for cross-contract integration
personToken.RoleRegistry = roleRegistry
// Set RoleRegistry on Vita for cross-contract integration
vita.RoleRegistry = roleRegistry
// Create VTS (Value Transfer System) contract
vts := newVTS()
vts.NEO = neo
vts.RoleRegistry = roleRegistry
vts.PersonToken = personToken
vts.Vita = vita
// Create Federation contract for cross-chain Vita coordination
federation := newFederation()
federation.NEO = neo
// Wire GAS dependencies for Vita fee exemption
gas.Vita = vita
gas.Federation = federation
gas.Treasury = treasury
return []interop.Contract{
mgmt,
@ -365,8 +417,9 @@ func NewDefaultContracts(cfg config.ProtocolConfiguration) []interop.Contract {
oracle,
notary,
treasury,
personToken,
vita,
roleRegistry,
vts,
federation,
}
}

View File

@ -0,0 +1,444 @@
package native
import (
"encoding/binary"
"errors"
"math/big"
"github.com/tutus-one/tutus-chain/pkg/config"
"github.com/tutus-one/tutus-chain/pkg/core/dao"
"github.com/tutus-one/tutus-chain/pkg/core/interop"
"github.com/tutus-one/tutus-chain/pkg/core/native/nativeids"
"github.com/tutus-one/tutus-chain/pkg/core/native/nativenames"
"github.com/tutus-one/tutus-chain/pkg/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"
)
// Federation represents the Federation native contract for cross-chain Vita coordination.
// It manages:
// - Visiting fee percentage (what % of fees the host chain pays for visitors)
// - Visitor registry (tracking Vita holders from other chains)
// - Inter-chain debt (tracking fees owed to other chains)
type Federation struct {
interop.ContractMD
NEO INEO
}
// Storage key prefixes for Federation.
const (
prefixVisitingFeePercent byte = 0x01 // -> uint8 (0-100, % host chain pays for visitors)
prefixVisitorRegistry byte = 0x02 // owner (Uint160) -> home chain ID (uint32)
prefixInterChainDebt byte = 0x03 // chain ID (uint32) -> *big.Int (amount owed)
)
// Default values.
const (
defaultVisitingFeePercent uint8 = 50 // 50% local, 50% inter-chain debt
)
// Event names for Federation.
const (
VisitorRegisteredEvent = "VisitorRegistered"
VisitorUnregisteredEvent = "VisitorUnregistered"
FeePercentChangedEvent = "FeePercentChanged"
DebtSettledEvent = "DebtSettled"
)
// Various errors.
var (
ErrInvalidFeePercent = errors.New("fee percent must be 0-100")
ErrVisitorAlreadyExists = errors.New("visitor already registered")
ErrVisitorNotFound = errors.New("visitor not found")
ErrInvalidChainID = errors.New("invalid chain ID")
ErrInsufficientDebt = errors.New("insufficient debt to settle")
)
var _ interop.Contract = (*Federation)(nil)
// newFederation creates a new Federation native contract.
func newFederation() *Federation {
f := &Federation{
ContractMD: *interop.NewContractMD(nativenames.Federation, nativeids.Federation),
}
defer f.BuildHFSpecificMD(f.ActiveIn())
// getFeePercent method
desc := NewDescriptor("getFeePercent", smartcontract.IntegerType)
md := NewMethodAndPrice(f.getFeePercent, 1<<15, callflag.ReadStates)
f.AddMethod(md, desc)
// setFeePercent method (committee only)
desc = NewDescriptor("setFeePercent", smartcontract.BoolType,
manifest.NewParameter("percent", smartcontract.IntegerType))
md = NewMethodAndPrice(f.setFeePercent, 1<<16, callflag.States|callflag.AllowNotify)
f.AddMethod(md, desc)
// registerVisitor method (committee only)
desc = NewDescriptor("registerVisitor", smartcontract.BoolType,
manifest.NewParameter("owner", smartcontract.Hash160Type),
manifest.NewParameter("homeChainID", smartcontract.IntegerType))
md = NewMethodAndPrice(f.registerVisitor, 1<<16, callflag.States|callflag.AllowNotify)
f.AddMethod(md, desc)
// unregisterVisitor method (committee only)
desc = NewDescriptor("unregisterVisitor", smartcontract.BoolType,
manifest.NewParameter("owner", smartcontract.Hash160Type))
md = NewMethodAndPrice(f.unregisterVisitor, 1<<16, callflag.States|callflag.AllowNotify)
f.AddMethod(md, desc)
// isVisitor method
desc = NewDescriptor("isVisitor", smartcontract.BoolType,
manifest.NewParameter("owner", smartcontract.Hash160Type))
md = NewMethodAndPrice(f.isVisitor, 1<<15, callflag.ReadStates)
f.AddMethod(md, desc)
// getHomeChain method
desc = NewDescriptor("getHomeChain", smartcontract.IntegerType,
manifest.NewParameter("owner", smartcontract.Hash160Type))
md = NewMethodAndPrice(f.getHomeChain, 1<<15, callflag.ReadStates)
f.AddMethod(md, desc)
// getInterChainDebt method
desc = NewDescriptor("getInterChainDebt", smartcontract.IntegerType,
manifest.NewParameter("chainID", smartcontract.IntegerType))
md = NewMethodAndPrice(f.getInterChainDebt, 1<<15, callflag.ReadStates)
f.AddMethod(md, desc)
// settleDebt method (committee only)
desc = NewDescriptor("settleDebt", smartcontract.BoolType,
manifest.NewParameter("chainID", smartcontract.IntegerType),
manifest.NewParameter("amount", smartcontract.IntegerType))
md = NewMethodAndPrice(f.settleDebt, 1<<16, callflag.States|callflag.AllowNotify)
f.AddMethod(md, desc)
// Events
eDesc := NewEventDescriptor(VisitorRegisteredEvent,
manifest.NewParameter("owner", smartcontract.Hash160Type),
manifest.NewParameter("homeChainID", smartcontract.IntegerType))
f.AddEvent(NewEvent(eDesc))
eDesc = NewEventDescriptor(VisitorUnregisteredEvent,
manifest.NewParameter("owner", smartcontract.Hash160Type))
f.AddEvent(NewEvent(eDesc))
eDesc = NewEventDescriptor(FeePercentChangedEvent,
manifest.NewParameter("oldPercent", smartcontract.IntegerType),
manifest.NewParameter("newPercent", smartcontract.IntegerType))
f.AddEvent(NewEvent(eDesc))
eDesc = NewEventDescriptor(DebtSettledEvent,
manifest.NewParameter("chainID", smartcontract.IntegerType),
manifest.NewParameter("amount", smartcontract.IntegerType),
manifest.NewParameter("remaining", smartcontract.IntegerType))
f.AddEvent(NewEvent(eDesc))
return f
}
// Metadata returns contract metadata.
func (f *Federation) Metadata() *interop.ContractMD {
return &f.ContractMD
}
// Initialize initializes Federation contract at the specified hardfork.
func (f *Federation) Initialize(ic *interop.Context, hf *config.Hardfork, newMD *interop.HFSpecificContractMD) error {
if hf != f.ActiveIn() {
return nil
}
// Initialize default fee percent
f.setFeePercentInternal(ic.DAO, defaultVisitingFeePercent)
return nil
}
// InitializeCache fills native Federation cache from DAO on node restart.
func (f *Federation) InitializeCache(_ interop.IsHardforkEnabled, blockHeight uint32, d *dao.Simple) error {
return nil
}
// OnPersist implements the Contract interface.
func (f *Federation) OnPersist(ic *interop.Context) error {
return nil
}
// PostPersist implements the Contract interface.
func (f *Federation) PostPersist(ic *interop.Context) error {
return nil
}
// ActiveIn returns the hardfork this contract activates in.
func (f *Federation) ActiveIn() *config.Hardfork {
var hf = config.HFFaun
return &hf
}
// Storage key helpers
func makeFeePercentKey() []byte {
return []byte{prefixVisitingFeePercent}
}
func makeVisitorKey(owner util.Uint160) []byte {
key := make([]byte, 1+util.Uint160Size)
key[0] = prefixVisitorRegistry
copy(key[1:], owner.BytesBE())
return key
}
func makeDebtKey(chainID uint32) []byte {
key := make([]byte, 5)
key[0] = prefixInterChainDebt
binary.BigEndian.PutUint32(key[1:], chainID)
return key
}
// Internal storage methods
func (f *Federation) getFeePercentInternal(d *dao.Simple) uint8 {
si := d.GetStorageItem(f.ID, makeFeePercentKey())
if si == nil {
return defaultVisitingFeePercent
}
return si[0]
}
func (f *Federation) setFeePercentInternal(d *dao.Simple, percent uint8) {
d.PutStorageItem(f.ID, makeFeePercentKey(), []byte{percent})
}
func (f *Federation) getVisitorChainInternal(d *dao.Simple, owner util.Uint160) (uint32, bool) {
si := d.GetStorageItem(f.ID, makeVisitorKey(owner))
if si == nil {
return 0, false
}
return binary.BigEndian.Uint32(si), true
}
func (f *Federation) setVisitorChainInternal(d *dao.Simple, owner util.Uint160, chainID uint32) {
buf := make([]byte, 4)
binary.BigEndian.PutUint32(buf, chainID)
d.PutStorageItem(f.ID, makeVisitorKey(owner), buf)
}
func (f *Federation) deleteVisitorInternal(d *dao.Simple, owner util.Uint160) {
d.DeleteStorageItem(f.ID, makeVisitorKey(owner))
}
func (f *Federation) getDebtInternal(d *dao.Simple, chainID uint32) *big.Int {
si := d.GetStorageItem(f.ID, makeDebtKey(chainID))
if si == nil {
return big.NewInt(0)
}
return new(big.Int).SetBytes(si)
}
func (f *Federation) setDebtInternal(d *dao.Simple, chainID uint32, amount *big.Int) {
if amount.Sign() <= 0 {
d.DeleteStorageItem(f.ID, makeDebtKey(chainID))
return
}
d.PutStorageItem(f.ID, makeDebtKey(chainID), amount.Bytes())
}
func (f *Federation) addDebtInternal(d *dao.Simple, chainID uint32, amount *big.Int) {
current := f.getDebtInternal(d, chainID)
newDebt := new(big.Int).Add(current, amount)
f.setDebtInternal(d, chainID, newDebt)
}
// Contract methods
func (f *Federation) getFeePercent(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
percent := f.getFeePercentInternal(ic.DAO)
return stackitem.NewBigInteger(big.NewInt(int64(percent)))
}
func (f *Federation) setFeePercent(ic *interop.Context, args []stackitem.Item) stackitem.Item {
percent := toBigInt(args[0]).Int64()
// Validate percent
if percent < 0 || percent > 100 {
panic(ErrInvalidFeePercent)
}
// Check committee
if !f.NEO.CheckCommittee(ic) {
panic(ErrNotCommittee)
}
// Get old value for event
oldPercent := f.getFeePercentInternal(ic.DAO)
// Set new value
f.setFeePercentInternal(ic.DAO, uint8(percent))
// Emit event
err := ic.AddNotification(f.Hash, FeePercentChangedEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(oldPercent))),
stackitem.NewBigInteger(big.NewInt(percent)),
}))
if err != nil {
panic(err)
}
return stackitem.NewBool(true)
}
func (f *Federation) registerVisitor(ic *interop.Context, args []stackitem.Item) stackitem.Item {
owner := toUint160(args[0])
chainID := uint32(toBigInt(args[1]).Int64())
// Validate chain ID
if chainID == 0 {
panic(ErrInvalidChainID)
}
// Check committee
if !f.NEO.CheckCommittee(ic) {
panic(ErrNotCommittee)
}
// Check if already registered
if _, exists := f.getVisitorChainInternal(ic.DAO, owner); exists {
panic(ErrVisitorAlreadyExists)
}
// Register visitor
f.setVisitorChainInternal(ic.DAO, owner, chainID)
// Emit event
err := ic.AddNotification(f.Hash, VisitorRegisteredEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewByteArray(owner.BytesBE()),
stackitem.NewBigInteger(big.NewInt(int64(chainID))),
}))
if err != nil {
panic(err)
}
return stackitem.NewBool(true)
}
func (f *Federation) unregisterVisitor(ic *interop.Context, args []stackitem.Item) stackitem.Item {
owner := toUint160(args[0])
// Check committee
if !f.NEO.CheckCommittee(ic) {
panic(ErrNotCommittee)
}
// Check if registered
if _, exists := f.getVisitorChainInternal(ic.DAO, owner); !exists {
panic(ErrVisitorNotFound)
}
// Unregister visitor
f.deleteVisitorInternal(ic.DAO, owner)
// Emit event
err := ic.AddNotification(f.Hash, VisitorUnregisteredEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewByteArray(owner.BytesBE()),
}))
if err != nil {
panic(err)
}
return stackitem.NewBool(true)
}
func (f *Federation) isVisitor(ic *interop.Context, args []stackitem.Item) stackitem.Item {
owner := toUint160(args[0])
_, exists := f.getVisitorChainInternal(ic.DAO, owner)
return stackitem.NewBool(exists)
}
func (f *Federation) getHomeChain(ic *interop.Context, args []stackitem.Item) stackitem.Item {
owner := toUint160(args[0])
chainID, exists := f.getVisitorChainInternal(ic.DAO, owner)
if !exists {
return stackitem.NewBigInteger(big.NewInt(0))
}
return stackitem.NewBigInteger(big.NewInt(int64(chainID)))
}
func (f *Federation) getInterChainDebt(ic *interop.Context, args []stackitem.Item) stackitem.Item {
chainID := uint32(toBigInt(args[0]).Int64())
debt := f.getDebtInternal(ic.DAO, chainID)
return stackitem.NewBigInteger(debt)
}
func (f *Federation) settleDebt(ic *interop.Context, args []stackitem.Item) stackitem.Item {
chainID := uint32(toBigInt(args[0]).Int64())
amount := toBigInt(args[1])
// Validate chain ID
if chainID == 0 {
panic(ErrInvalidChainID)
}
// Check committee
if !f.NEO.CheckCommittee(ic) {
panic(ErrNotCommittee)
}
// Get current debt
currentDebt := f.getDebtInternal(ic.DAO, chainID)
// Check sufficient debt
if currentDebt.Cmp(amount) < 0 {
panic(ErrInsufficientDebt)
}
// Subtract settled amount
remaining := new(big.Int).Sub(currentDebt, amount)
f.setDebtInternal(ic.DAO, chainID, remaining)
// Emit event
err := ic.AddNotification(f.Hash, DebtSettledEvent, stackitem.NewArray([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(chainID))),
stackitem.NewBigInteger(amount),
stackitem.NewBigInteger(remaining),
}))
if err != nil {
panic(err)
}
return stackitem.NewBool(true)
}
// Public methods for cross-native access
// GetVisitingFeePercent returns the visiting fee percent (for cross-native access).
func (f *Federation) GetVisitingFeePercent(d *dao.Simple) uint8 {
return f.getFeePercentInternal(d)
}
// IsVisitor checks if an address is a registered visitor (for cross-native access).
func (f *Federation) IsVisitor(d *dao.Simple, owner util.Uint160) bool {
_, exists := f.getVisitorChainInternal(d, owner)
return exists
}
// GetHomeChain returns the home chain ID for a visitor (for cross-native access).
func (f *Federation) GetHomeChain(d *dao.Simple, owner util.Uint160) uint32 {
chainID, _ := f.getVisitorChainInternal(d, owner)
return chainID
}
// AddInterChainDebt adds to the inter-chain debt for a specific chain (for cross-native access).
func (f *Federation) AddInterChainDebt(d *dao.Simple, chainID uint32, amount *big.Int) {
f.addDebtInternal(d, chainID, amount)
}
// GetInterChainDebt returns the inter-chain debt for a specific chain (for cross-native access).
func (f *Federation) GetInterChainDebt(d *dao.Simple, chainID uint32) *big.Int {
return f.getDebtInternal(d, chainID)
}
// Address returns the contract's script hash.
func (f *Federation) Address() util.Uint160 {
return f.Hash
}

View File

@ -20,12 +20,27 @@ import (
// GAS represents GAS native contract.
type GAS struct {
nep17TokenNative
NEO INEO
Policy IPolicy
NEO INEO
Policy IPolicy
Vita IVita // For checking local Vita status
Federation IFederation // For visiting Vita and fee split
Treasury ITreasury // For subsidizing fees
initialSupply int64
}
// VitaExemptType indicates the type of Vita exemption for fee purposes.
type VitaExemptType int
const (
// VitaExemptNone indicates no Vita exemption.
VitaExemptNone VitaExemptType = iota
// VitaExemptLocal indicates a local Vita holder (100% paid by local Treasury).
VitaExemptLocal
// VitaExemptVisitor indicates a visiting Vita holder (split between local and home chain).
VitaExemptVisitor
)
// GASFactor is a divisor for finding GAS integral value.
const GASFactor = NEOTotalSupply
@ -112,7 +127,26 @@ func (g *GAS) OnPersist(ic *interop.Context) error {
}
for _, tx := range ic.Block.Transactions {
absAmount := big.NewInt(tx.SystemFee + tx.NetworkFee)
g.Burn(ic, tx.Sender(), absAmount)
sender := tx.Sender()
// Check fee exemption type
exemptType := g.getVitaExemptType(ic.DAO, sender)
switch exemptType {
case VitaExemptLocal:
// Local citizen: 100% from Treasury
g.burnFromTreasury(ic, absAmount)
case VitaExemptVisitor:
// Visitor: split between local Treasury and inter-chain debt
var homeChain uint32
if g.Federation != nil {
homeChain = g.Federation.GetHomeChain(ic.DAO, sender)
}
g.burnFromTreasuryWithSplit(ic, absAmount, homeChain)
default:
// Non-citizen: burn from sender
g.Burn(ic, sender, absAmount)
}
}
validators := g.NEO.GetNextBlockValidatorsInternal(ic.DAO)
primary := validators[ic.Block.PrimaryIndex].GetScriptHash()
@ -136,6 +170,67 @@ func (g *GAS) PostPersist(ic *interop.Context) error {
return nil
}
// getVitaExemptType determines if sender is exempt from fees and what type.
func (g *GAS) getVitaExemptType(d *dao.Simple, sender util.Uint160) VitaExemptType {
// Check local Vita first
if g.Vita != nil {
token, err := g.Vita.GetTokenByOwner(d, sender)
if err == nil && token != nil && token.Status == state.TokenStatusActive {
return VitaExemptLocal
}
}
// Check visitor registry
if g.Federation != nil && g.Federation.IsVisitor(d, sender) {
return VitaExemptVisitor
}
return VitaExemptNone
}
// burnFromTreasury burns GAS from Treasury for local Vita holders (100% local).
func (g *GAS) burnFromTreasury(ic *interop.Context, amount *big.Int) {
g.burnFromTreasuryInternal(ic, amount, 100, 0)
}
// burnFromTreasuryWithSplit burns GAS with visiting fee split between local Treasury and inter-chain debt.
func (g *GAS) burnFromTreasuryWithSplit(ic *interop.Context, amount *big.Int, homeChain uint32) {
localPercent := uint8(100)
if g.Federation != nil {
localPercent = g.Federation.GetVisitingFeePercent(ic.DAO)
}
g.burnFromTreasuryInternal(ic, amount, localPercent, homeChain)
}
// burnFromTreasuryInternal handles the actual burn with optional split between local and inter-chain.
func (g *GAS) burnFromTreasuryInternal(ic *interop.Context, amount *big.Int, localPercent uint8, homeChain uint32) {
if g.Treasury == nil {
return
}
treasuryAddr := g.Treasury.Address()
treasuryBalance := g.BalanceOf(ic.DAO, treasuryAddr)
// Calculate local vs inter-chain portions
localAmount := new(big.Int).Mul(amount, big.NewInt(int64(localPercent)))
localAmount.Div(localAmount, big.NewInt(100))
interChainAmount := new(big.Int).Sub(amount, localAmount)
// Burn from Treasury what we can
if treasuryBalance.Cmp(localAmount) >= 0 {
g.Burn(ic, treasuryAddr, localAmount)
} else if treasuryBalance.Sign() > 0 {
deficit := new(big.Int).Sub(localAmount, treasuryBalance)
g.Burn(ic, treasuryAddr, treasuryBalance)
g.Treasury.AddDeficit(ic.DAO, deficit)
} else {
g.Treasury.AddDeficit(ic.DAO, localAmount)
}
// Record inter-chain debt (if visitor)
if interChainAmount.Sign() > 0 && homeChain != 0 && g.Federation != nil {
g.Federation.AddInterChainDebt(ic.DAO, homeChain, interChainAmount)
}
}
// ActiveIn implements the Contract interface.
func (g *GAS) ActiveIn() *config.Hardfork {
return nil

File diff suppressed because one or more lines are too long

View File

@ -11,13 +11,13 @@ import (
"github.com/tutus-one/tutus-chain/pkg/vm/stackitem"
)
func newPersonTokenClient(t *testing.T) *neotest.ContractInvoker {
return newNativeClient(t, nativenames.PersonToken)
func newVitaClient(t *testing.T) *neotest.ContractInvoker {
return newNativeClient(t, nativenames.Vita)
}
// registerPersonToken is a helper to register a PersonToken for a signer.
// registerVita is a helper to register a Vita for a signer.
// Returns the tokenID bytes.
func registerPersonToken(t *testing.T, c *neotest.ContractInvoker, signer neotest.Signer) []byte {
func registerVita(t *testing.T, c *neotest.ContractInvoker, signer neotest.Signer) []byte {
owner := signer.ScriptHash()
personHash := hash.Sha256(owner.BytesBE()).BytesBE()
isEntity := false
@ -38,9 +38,9 @@ func registerPersonToken(t *testing.T, c *neotest.ContractInvoker, signer neotes
return tokenIDBytes
}
// TestPersonToken_Register tests basic registration functionality.
func TestPersonToken_Register(t *testing.T) {
c := newPersonTokenClient(t)
// TestVita_Register tests basic registration functionality.
func TestVita_Register(t *testing.T) {
c := newVitaClient(t)
e := c.Executor
acc := e.NewAccount(t)
@ -61,7 +61,7 @@ func TestPersonToken_Register(t *testing.T) {
// Check event was emitted
aer := e.GetTxExecResult(t, txHash)
require.Equal(t, 1, len(aer.Events))
require.Equal(t, "PersonTokenCreated", aer.Events[0].Name)
require.Equal(t, "VitaCreated", aer.Events[0].Name)
// Check exists returns true
invoker.Invoke(t, true, "exists", owner.BytesBE())
@ -74,13 +74,13 @@ func TestPersonToken_Register(t *testing.T) {
}, "getToken", owner.BytesBE())
}
// TestPersonToken_ValidateCaller tests the validateCaller method.
// TestVita_ValidateCaller tests the validateCaller method.
// Note: validateCaller uses GetCallingScriptHash() which returns the calling contract's
// script hash, not the transaction signer's address. When called directly from a transaction
// (not from another contract), the caller has no token. These methods are designed for
// cross-contract authorization.
func TestPersonToken_ValidateCaller(t *testing.T) {
c := newPersonTokenClient(t)
func TestVita_ValidateCaller(t *testing.T) {
c := newVitaClient(t)
t.Run("no token - direct call", func(t *testing.T) {
acc := c.Executor.NewAccount(t)
@ -88,35 +88,35 @@ func TestPersonToken_ValidateCaller(t *testing.T) {
// validateCaller uses GetCallingScriptHash() which returns the transaction script hash
// when called directly, not the signer's account. This will always fail for direct calls.
invoker.InvokeFail(t, "caller does not have a PersonToken", "validateCaller")
invoker.InvokeFail(t, "caller does not have a Vita", "validateCaller")
})
// Note: Testing validateCaller with a token requires deploying a helper contract
// that has a PersonToken registered to its script hash, then calling validateCaller
// that has a Vita registered to its script hash, then calling validateCaller
// from within that contract. This is the intended usage pattern for cross-contract auth.
}
// TestPersonToken_RequireRole tests the requireRole method.
// TestVita_RequireRole tests the requireRole method.
// Note: requireRole uses GetCallingScriptHash() - designed for cross-contract authorization.
func TestPersonToken_RequireRole(t *testing.T) {
c := newPersonTokenClient(t)
func TestVita_RequireRole(t *testing.T) {
c := newVitaClient(t)
t.Run("no token - direct call", func(t *testing.T) {
acc := c.Executor.NewAccount(t)
invoker := c.WithSigners(acc)
// Direct calls always fail because GetCallingScriptHash() returns transaction script hash
invoker.InvokeFail(t, "caller does not have a PersonToken", "requireRole", 0)
invoker.InvokeFail(t, "caller does not have a Vita", "requireRole", 0)
})
// Note: Testing requireRole with actual role checks requires a deployed contract
// with a PersonToken registered to its script hash.
// with a Vita registered to its script hash.
}
// TestPersonToken_RequireCoreRole tests the requireCoreRole method.
// TestVita_RequireCoreRole tests the requireCoreRole method.
// Note: requireCoreRole uses GetCallingScriptHash() - designed for cross-contract authorization.
func TestPersonToken_RequireCoreRole(t *testing.T) {
c := newPersonTokenClient(t)
func TestVita_RequireCoreRole(t *testing.T) {
c := newVitaClient(t)
// CoreRole constants
const (
@ -137,17 +137,17 @@ func TestPersonToken_RequireCoreRole(t *testing.T) {
invoker := c.WithSigners(acc)
// Direct calls always fail because GetCallingScriptHash() returns transaction script hash
invoker.InvokeFail(t, "caller does not have a PersonToken", "requireCoreRole", CoreRoleNone)
invoker.InvokeFail(t, "caller does not have a Vita", "requireCoreRole", CoreRoleNone)
})
// Note: Testing requireCoreRole with actual role checks requires a deployed contract
// with a PersonToken registered to its script hash.
// with a Vita registered to its script hash.
}
// TestPersonToken_RequirePermission tests the requirePermission method.
// TestVita_RequirePermission tests the requirePermission method.
// Note: requirePermission uses GetCallingScriptHash() - designed for cross-contract authorization.
func TestPersonToken_RequirePermission(t *testing.T) {
c := newPersonTokenClient(t)
func TestVita_RequirePermission(t *testing.T) {
c := newVitaClient(t)
t.Run("empty resource", func(t *testing.T) {
acc := c.Executor.NewAccount(t)
@ -170,16 +170,16 @@ func TestPersonToken_RequirePermission(t *testing.T) {
invoker := c.WithSigners(acc)
// Direct calls always fail because GetCallingScriptHash() returns transaction script hash
invoker.InvokeFail(t, "caller does not have a PersonToken", "requirePermission", "documents", "read", "global")
invoker.InvokeFail(t, "caller does not have a Vita", "requirePermission", "documents", "read", "global")
})
// Note: Testing requirePermission with actual permission checks requires a deployed contract
// with a PersonToken registered to its script hash.
// with a Vita registered to its script hash.
}
// TestPersonToken_TotalSupply tests the totalSupply method.
func TestPersonToken_TotalSupply(t *testing.T) {
c := newPersonTokenClient(t)
// TestVita_TotalSupply tests the totalSupply method.
func TestVita_TotalSupply(t *testing.T) {
c := newVitaClient(t)
e := c.Executor
// Initially, totalSupply should be 0
@ -187,27 +187,27 @@ func TestPersonToken_TotalSupply(t *testing.T) {
// Register a token
acc1 := e.NewAccount(t)
registerPersonToken(t, c, acc1)
registerVita(t, c, acc1)
// Now totalSupply should be 1
c.Invoke(t, 1, "totalSupply")
// Register another token
acc2 := e.NewAccount(t)
registerPersonToken(t, c, acc2)
registerVita(t, c, acc2)
// Now totalSupply should be 2
c.Invoke(t, 2, "totalSupply")
}
// TestPersonToken_GetTokenByID tests the getTokenByID method.
func TestPersonToken_GetTokenByID(t *testing.T) {
c := newPersonTokenClient(t)
// TestVita_GetTokenByID tests the getTokenByID method.
func TestVita_GetTokenByID(t *testing.T) {
c := newVitaClient(t)
e := c.Executor
// Register a token - the first token gets ID 0 (counter starts at 0)
acc := e.NewAccount(t)
registerPersonToken(t, c, acc)
registerVita(t, c, acc)
// Token ID 0 should exist (first registered token)
c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {
@ -215,7 +215,7 @@ func TestPersonToken_GetTokenByID(t *testing.T) {
// Check that result is an array (not null)
arr, ok := stack[0].Value().([]stackitem.Item)
require.True(t, ok, "expected array result for existing token")
require.GreaterOrEqual(t, len(arr), 9) // PersonToken has 9 fields
require.GreaterOrEqual(t, len(arr), 9) // Vita has 9 fields
// Check owner matches (owner is at index 1)
owner, ok := arr[1].Value().([]byte)
@ -231,13 +231,13 @@ func TestPersonToken_GetTokenByID(t *testing.T) {
}, "getTokenByID", 999999)
}
// TestPersonToken_SuspendReinstate tests suspend and reinstate functionality.
func TestPersonToken_SuspendReinstate(t *testing.T) {
c := newPersonTokenClient(t)
// TestVita_SuspendReinstate tests suspend and reinstate functionality.
func TestVita_SuspendReinstate(t *testing.T) {
c := newVitaClient(t)
e := c.Executor
acc := e.NewAccount(t)
registerPersonToken(t, c, acc)
registerVita(t, c, acc)
invoker := c.WithSigners(acc)
committeeInvoker := c.WithSigners(c.Committee)
@ -285,6 +285,6 @@ func TestPersonToken_SuspendReinstate(t *testing.T) {
// Note: Full cross-contract testing of validateCaller, requireRole, requireCoreRole, and
// requirePermission would require deploying a helper contract that:
// 1. Has a PersonToken registered to its script hash
// 2. Calls the PersonToken cross-contract methods from within its own methods
// 1. Has a Vita registered to its script hash
// 2. Calls the Vita cross-contract methods from within its own methods
// This is the intended usage pattern for these cross-contract authorization methods.

View File

@ -15,12 +15,12 @@ func newVTSClient(t *testing.T) *neotest.ContractInvoker {
return newNativeClient(t, nativenames.VTS)
}
// registerVita is a helper to register a PersonToken (Vita) for an account.
// registerVitaForVTS is a helper to register a Vita for an account.
// This is required for accounts receiving restricted VTS.
// Uses the existing executor to ensure we're on the same blockchain.
func registerVita(t *testing.T, e *neotest.Executor, acc neotest.Signer) {
personTokenHash := e.NativeHash(t, nativenames.PersonToken)
c := e.NewInvoker(personTokenHash, acc)
func registerVitaForVTS(t *testing.T, e *neotest.Executor, acc neotest.Signer) {
vitaHash := e.NativeHash(t, nativenames.Vita)
c := e.NewInvoker(vitaHash, acc)
owner := acc.ScriptHash()
personHash := hash.Sha256(owner.BytesBE()).BytesBE()
isEntity := false
@ -37,8 +37,8 @@ func registerVita(t *testing.T, e *neotest.Executor, acc neotest.Signer) {
// This is required for spending at age-restricted vendors.
// Uses the existing executor to ensure we're on the same blockchain.
func addAgeVerifiedAttribute(t *testing.T, e *neotest.Executor, committee neotest.Signer, acc neotest.Signer) {
personTokenHash := e.NativeHash(t, nativenames.PersonToken)
c := e.CommitteeInvoker(personTokenHash)
vitaHash := e.NativeHash(t, nativenames.Vita)
c := e.CommitteeInvoker(vitaHash)
owner := acc.ScriptHash()
// Get the token ID from getToken result
@ -126,7 +126,7 @@ func TestVTS_MintRestricted(t *testing.T) {
committeeInvoker := c.WithSigners(c.Committee)
// Restricted VTS requires recipient to have a Vita
registerVita(t, e, acc)
registerVitaForVTS(t, e, acc)
t.Run("mint food-restricted VTS", func(t *testing.T) {
committeeInvoker.Invoke(t, true, "mintRestricted", acc.ScriptHash(), 500_00000000, state.CategoryFood)
@ -334,7 +334,7 @@ func TestVTS_ConvertToUnrestricted(t *testing.T) {
userInvoker := c.WithSigners(acc)
// Restricted VTS requires Vita
registerVita(t, e, acc)
registerVitaForVTS(t, e, acc)
// Mint restricted VTS
committeeInvoker.Invoke(t, true, "mintRestricted", acc.ScriptHash(), 500_00000000, state.CategoryFood)
@ -460,7 +460,7 @@ func TestVTS_BalanceDetails(t *testing.T) {
committeeInvoker := c.WithSigners(c.Committee)
// Restricted VTS requires Vita
registerVita(t, e, acc)
registerVitaForVTS(t, e, acc)
// Mint mixed VTS
committeeInvoker.Invoke(t, true, "mint", acc.ScriptHash(), 100_00000000)
@ -542,7 +542,7 @@ func TestVTS_NegativeAmount(t *testing.T) {
committeeInvoker := c.WithSigners(c.Committee)
// Restricted VTS requires Vita
registerVita(t, e, acc)
registerVitaForVTS(t, e, acc)
t.Run("mint negative fails", func(t *testing.T) {
committeeInvoker.InvokeFail(t, "amount must be positive", "mint", acc.ScriptHash(), -1)
@ -564,7 +564,7 @@ func TestVTS_MultiCategoryVendor(t *testing.T) {
customerInvoker := c.WithSigners(customer)
// Restricted VTS requires Vita
registerVita(t, e, customer)
registerVitaForVTS(t, e, customer)
// Register vendor accepting food AND shelter
categories := state.CategoryFood | state.CategoryShelter
@ -605,8 +605,8 @@ func TestVTS_AgeRestrictedVendor(t *testing.T) {
minorInvoker := c.WithSigners(minorCustomer)
// Register Vita for both customers
registerVita(t, e, adultCustomer)
registerVita(t, e, minorCustomer)
registerVitaForVTS(t, e, adultCustomer)
registerVitaForVTS(t, e, minorCustomer)
// Only adult has age_verified attribute
addAgeVerifiedAttribute(t, e, c.Committee, adultCustomer)
@ -645,10 +645,10 @@ func TestVTS_MintRestrictedRequiresVita(t *testing.T) {
committeeInvoker := c.WithSigners(c.Committee)
// Only register Vita for one account
registerVita(t, e, accWithVita)
registerVitaForVTS(t, e, accWithVita)
t.Run("cannot mint restricted VTS to account without Vita", func(t *testing.T) {
committeeInvoker.InvokeFail(t, "restricted VTS requires Vita (PersonToken)", "mintRestricted", accWithoutVita.ScriptHash(), 100_00000000, state.CategoryFood)
committeeInvoker.InvokeFail(t, "restricted VTS requires Vita", "mintRestricted", accWithoutVita.ScriptHash(), 100_00000000, state.CategoryFood)
})
t.Run("can mint restricted VTS to account with Vita", func(t *testing.T) {

View File

@ -31,10 +31,12 @@ 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}
// Vita is a hash of native Vita contract.
Vita = util.Uint160{0x7d, 0x21, 0xb3, 0xd4, 0x1d, 0x79, 0xd0, 0x82, 0xcb, 0x1a, 0x24, 0xc8, 0xf9, 0xb8, 0xdf, 0x3f, 0x4, 0x7f, 0x43, 0xde}
// 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}
// Federation is a hash of native Federation contract.
Federation = util.Uint160{0xfd, 0x78, 0x70, 0xb, 0xa9, 0x19, 0xed, 0xb0, 0x19, 0x40, 0xdd, 0xc7, 0x97, 0x48, 0xf4, 0x25, 0x1d, 0xb0, 0x5, 0x1f}
)

View File

@ -29,10 +29,12 @@ const (
Notary int32 = -10
// Treasury is an ID of native Treasury contract.
Treasury int32 = -11
// PersonToken is an ID of native PersonToken contract.
PersonToken int32 = -12
// Vita is an ID of native Vita contract.
Vita int32 = -12
// RoleRegistry is an ID of native RoleRegistry contract.
RoleRegistry int32 = -13
// VTS is an ID of native VTS contract.
VTS int32 = -14
// Federation is an ID of native Federation contract.
Federation int32 = -15
)

View File

@ -13,9 +13,10 @@ const (
CryptoLib = "CryptoLib"
StdLib = "StdLib"
Treasury = "Treasury"
PersonToken = "PersonToken"
Vita = "Vita"
RoleRegistry = "RoleRegistry"
VTS = "VTS"
Federation = "Federation"
)
// All contains the list of all native contract names ordered by the contract ID.
@ -31,9 +32,10 @@ var All = []string{
Oracle,
Notary,
Treasury,
PersonToken,
Vita,
RoleRegistry,
VTS,
Federation,
}
// IsValid checks if the name is a valid native contract's name.
@ -49,7 +51,8 @@ func IsValid(name string) bool {
name == CryptoLib ||
name == StdLib ||
name == Treasury ||
name == PersonToken ||
name == Vita ||
name == RoleRegistry ||
name == VTS
name == VTS ||
name == Federation
}

View File

@ -22,7 +22,7 @@ import (
)
// RoleRegistry is a native contract for hierarchical role-based access control.
// It replaces NEO.CheckCommittee() with democratic governance based on Vita (PersonToken).
// It replaces NEO.CheckCommittee() with democratic governance based on Vita.
type RoleRegistry struct {
interop.ContractMD
@ -42,7 +42,7 @@ type RoleRegistryCache struct {
const (
// RoleCommittee is the Tutus Committee role (designated officials).
RoleCommittee uint64 = 1
// RoleRegistrar can register new PersonTokens.
// RoleRegistrar can register new Vitas.
RoleRegistrar uint64 = 2
// RoleAttestor can attest identity attributes.
RoleAttestor uint64 = 3
@ -269,7 +269,7 @@ func (r *RoleRegistry) Initialize(ic *interop.Context, hf *config.Hardfork, newM
description string
}{
{RoleCommittee, "COMMITTEE", "Tutus Committee member (designated officials)"},
{RoleRegistrar, "REGISTRAR", "Can register new PersonTokens"},
{RoleRegistrar, "REGISTRAR", "Can register new Vitas"},
{RoleAttestor, "ATTESTOR", "Can attest identity attributes"},
{RoleOperator, "OPERATOR", "System operator (node management)"},
}

View File

@ -1,6 +1,8 @@
package native
import (
"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"
@ -9,6 +11,7 @@ import (
"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"
)
@ -18,6 +21,11 @@ type Treasury struct {
NEO INEO
}
// Storage key prefixes for Treasury.
const (
prefixTreasuryDeficit byte = 0x01 // -> *big.Int (cumulative deficit from fee subsidies)
)
var _ interop.Contract = (*Treasury)(nil)
func newTreasury() *Treasury {
@ -105,3 +113,36 @@ func (t *Treasury) onNEP17Payment(ic *interop.Context, args []stackitem.Item) st
var _, _, _ = from, amount, data
return stackitem.Null{}
}
// Public methods for cross-native access
// Address returns the contract's script hash.
func (t *Treasury) Address() util.Uint160 {
return t.Hash
}
// Storage key helpers
func makeDeficitKey() []byte {
return []byte{prefixTreasuryDeficit}
}
// AddDeficit adds to the cumulative deficit (for cross-native access).
// This is called when Treasury cannot cover fee subsidies for Vita holders.
func (t *Treasury) AddDeficit(d *dao.Simple, amount *big.Int) {
if amount.Sign() <= 0 {
return
}
current := t.GetDeficit(d)
newDeficit := new(big.Int).Add(current, amount)
d.PutStorageItem(t.ID, makeDeficitKey(), newDeficit.Bytes())
}
// GetDeficit returns the current cumulative deficit (for cross-native access).
func (t *Treasury) GetDeficit(d *dao.Simple) *big.Int {
si := d.GetStorageItem(t.ID, makeDeficitKey())
if si == nil {
return big.NewInt(0)
}
return new(big.Int).SetBytes(si)
}

File diff suppressed because it is too large Load Diff

View File

@ -41,7 +41,7 @@ type VTS struct {
NEO INEO
RoleRegistry IRoleRegistry
PersonToken IPersonToken
Vita IVita
symbol string
decimals int64
@ -646,10 +646,10 @@ func (v *VTS) mintRestricted(ic *interop.Context, args []stackitem.Item) stackit
panic("use mint() for unrestricted tokens")
}
// Restricted VTS requires recipient to have a Vita (PersonToken)
// Restricted VTS requires recipient to have a Vita
// This ensures benefits go to verified identities
if !v.PersonToken.TokenExists(ic.DAO, to) {
panic("restricted VTS requires Vita (PersonToken)")
if !v.Vita.TokenExists(ic.DAO, to) {
panic("restricted VTS requires Vita")
}
v.mintRestrictedInternal(ic, to, amount, category)
@ -948,7 +948,7 @@ func (v *VTS) spend(ic *interop.Context, args []stackitem.Item) stackitem.Item {
// Check age verification for age-restricted vendors (e.g., alcohol, tobacco)
if vendorInfo.AgeRestricted {
if !v.PersonToken.IsAdultVerified(ic.DAO, from) {
if !v.Vita.IsAdultVerified(ic.DAO, from) {
panic("age verification required for this vendor")
}
}
@ -1027,7 +1027,7 @@ func (v *VTS) canSpendAt(ic *interop.Context, args []stackitem.Item) stackitem.I
// Check age verification for age-restricted vendors
if vendorInfo.AgeRestricted {
if !v.PersonToken.IsAdultVerified(ic.DAO, account) {
if !v.Vita.IsAdultVerified(ic.DAO, account) {
return stackitem.NewBool(false)
}
}

View File

@ -102,9 +102,9 @@ func (r *Role) FromStackItem(item stackitem.Item) error {
return nil
}
// RoleAssignment represents a role granted to a PersonToken holder.
// RoleAssignment represents a role granted to a Vita holder.
type RoleAssignment struct {
TokenID uint64 // PersonToken ID (or script hash as uint64 for address-based)
TokenID uint64 // Vita ID (or script hash as uint64 for address-based)
RoleID uint64 // Role ID
GrantedAt uint32 // Block height when granted
GrantedBy util.Uint160 // Granter's script hash
@ -250,7 +250,7 @@ func (p *PermissionGrant) FromStackItem(item stackitem.Item) error {
}
// AddressRoleAssignment represents a role granted directly to an address (script hash).
// This is used for bootstrapping when PersonTokens may not exist yet.
// This is used for bootstrapping when Vitas may not exist yet.
type AddressRoleAssignment struct {
Address util.Uint160 // Script hash of the address
RoleID uint64 // Role ID

View File

@ -10,7 +10,7 @@ import (
"github.com/tutus-one/tutus-chain/pkg/vm/stackitem"
)
// TokenStatus represents the status of a PersonToken.
// TokenStatus represents the status of a Vita token.
type TokenStatus uint8
const (
@ -54,8 +54,8 @@ const (
RecoveryStatusExpired RecoveryStatus = 4
)
// PersonToken represents a soul-bound identity token.
type PersonToken struct {
// Vita represents a soul-bound identity token.
type Vita struct {
TokenID uint64 // Unique sequential identifier
Owner util.Uint160 // Owner's script hash
PersonHash []byte // Hash of biometric/identity proof
@ -68,7 +68,7 @@ type PersonToken struct {
}
// ToStackItem implements stackitem.Convertible interface.
func (t *PersonToken) ToStackItem() (stackitem.Item, error) {
func (t *Vita) ToStackItem() (stackitem.Item, error) {
return stackitem.NewStruct([]stackitem.Item{
stackitem.NewBigInteger(big.NewInt(int64(t.TokenID))),
stackitem.NewByteArray(t.Owner.BytesBE()),
@ -83,7 +83,7 @@ func (t *PersonToken) ToStackItem() (stackitem.Item, error) {
}
// FromStackItem implements stackitem.Convertible interface.
func (t *PersonToken) FromStackItem(item stackitem.Item) error {
func (t *Vita) FromStackItem(item stackitem.Item) error {
items, ok := item.Value().([]stackitem.Item)
if !ok {
return errors.New("not a struct")
@ -248,7 +248,7 @@ func (a *Attribute) FromStackItem(item stackitem.Item) error {
// AuthChallenge represents a passwordless authentication challenge.
type AuthChallenge struct {
ChallengeID util.Uint256 // Unique challenge identifier
TokenID uint64 // Associated PersonToken
TokenID uint64 // Associated Vita token
Nonce []byte // Random bytes to sign
CreatedAt uint32 // Block height when created
ExpiresAt uint32 // Block height when expires

View File

@ -26,6 +26,12 @@ func (f NotaryFeer) BlockHeight() uint32 {
return f.bc.BlockHeight()
}
// IsVitaFeeExempt implements mempool.Feer interface.
// Notary requests are not subject to Vita fee exemption.
func (f NotaryFeer) IsVitaFeeExempt(util.Uint160) bool {
return false
}
// NewNotaryFeer returns new NotaryFeer instance.
func NewNotaryFeer(bc Ledger) NotaryFeer {
return NotaryFeer{