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 }