diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 9c30a97..43f444d 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -2458,7 +2458,7 @@ func (bc *Blockchain) GetUtilityTokenBalance(acc util.Uint160) *big.Int { } // IsVitaFeeExempt returns true if the account has an active Vita token -// (local or visiting) and is exempt from paying transaction fees. +// (local, naturalized, visiting, or asylum) and is exempt from paying transaction fees. // This implements the Feer interface. func (bc *Blockchain) IsVitaFeeExempt(acc util.Uint160) bool { // Check local Vita first @@ -2468,11 +2468,21 @@ func (bc *Blockchain) IsVitaFeeExempt(acc util.Uint160) bool { return true } } - // Check visiting Vita registry + + // Check federation-based exemptions if bc.federation != nil { + // Naturalized citizens are exempt (treated same as local) + if bc.federation.IsNaturalizedCitizen(bc.dao, acc) { + return true + } + // Visitors are exempt if bc.federation.IsVisitor(bc.dao, acc) { return true } + // Asylum seekers are exempt (humanitarian override) + if bc.federation.HasAsylum(bc.dao, acc) { + return true + } } return false } diff --git a/pkg/core/native/contract.go b/pkg/core/native/contract.go index 9c185e0..6f0fdca 100644 --- a/pkg/core/native/contract.go +++ b/pkg/core/native/contract.go @@ -174,6 +174,10 @@ type ( GetInterChainDebt(d *dao.Simple, chainID uint32) *big.Int // Address returns the contract's script hash. Address() util.Uint160 + // HasAsylum checks if an address has asylum status (humanitarian override). + HasAsylum(d *dao.Simple, owner util.Uint160) bool + // IsNaturalizedCitizen checks if an address is a naturalized citizen (permanent immigration). + IsNaturalizedCitizen(d *dao.Simple, owner util.Uint160) bool } // ITreasury is an interface required from native Treasury contract for diff --git a/pkg/core/native/federation.go b/pkg/core/native/federation.go index cf92a8d..ede2f9a 100644 --- a/pkg/core/native/federation.go +++ b/pkg/core/native/federation.go @@ -32,6 +32,8 @@ 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) + prefixAsylumRegistry byte = 0x04 // owner (Uint160) -> AsylumRecord + prefixNaturalizedCitizen byte = 0x05 // owner (Uint160) -> NaturalizationRecord ) // Default values. @@ -45,15 +47,22 @@ const ( VisitorUnregisteredEvent = "VisitorUnregistered" FeePercentChangedEvent = "FeePercentChanged" DebtSettledEvent = "DebtSettled" + AsylumGrantedEvent = "AsylumGranted" + AsylumRevokedEvent = "AsylumRevoked" + CitizenNaturalizedEvent = "CitizenNaturalized" ) // 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") + 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") + ErrAsylumAlreadyGranted = errors.New("asylum already granted") + ErrAsylumNotFound = errors.New("asylum not found") + ErrAlreadyNaturalized = errors.New("already naturalized") + ErrNotNaturalized = errors.New("not naturalized") ) var _ interop.Contract = (*Federation)(nil) @@ -114,6 +123,51 @@ func newFederation() *Federation { md = NewMethodAndPrice(f.settleDebt, 1<<16, callflag.States|callflag.AllowNotify) f.AddMethod(md, desc) + // grantAsylum method (committee only) + desc = NewDescriptor("grantAsylum", smartcontract.BoolType, + manifest.NewParameter("owner", smartcontract.Hash160Type), + manifest.NewParameter("homeChainID", smartcontract.IntegerType), + manifest.NewParameter("reason", smartcontract.StringType)) + md = NewMethodAndPrice(f.grantAsylum, 1<<16, callflag.States|callflag.AllowNotify) + f.AddMethod(md, desc) + + // revokeAsylum method (committee only) + desc = NewDescriptor("revokeAsylum", smartcontract.BoolType, + manifest.NewParameter("owner", smartcontract.Hash160Type)) + md = NewMethodAndPrice(f.revokeAsylum, 1<<16, callflag.States|callflag.AllowNotify) + f.AddMethod(md, desc) + + // hasAsylum method + desc = NewDescriptor("hasAsylum", smartcontract.BoolType, + manifest.NewParameter("owner", smartcontract.Hash160Type)) + md = NewMethodAndPrice(f.hasAsylum, 1<<15, callflag.ReadStates) + f.AddMethod(md, desc) + + // getAsylumInfo method + desc = NewDescriptor("getAsylumInfo", smartcontract.ArrayType, + manifest.NewParameter("owner", smartcontract.Hash160Type)) + md = NewMethodAndPrice(f.getAsylumInfo, 1<<15, callflag.ReadStates) + f.AddMethod(md, desc) + + // naturalize method (committee only) + desc = NewDescriptor("naturalize", smartcontract.BoolType, + manifest.NewParameter("owner", smartcontract.Hash160Type), + manifest.NewParameter("originalHomeChain", smartcontract.IntegerType)) + md = NewMethodAndPrice(f.naturalize, 1<<16, callflag.States|callflag.AllowNotify) + f.AddMethod(md, desc) + + // isNaturalizedCitizen method + desc = NewDescriptor("isNaturalizedCitizen", smartcontract.BoolType, + manifest.NewParameter("owner", smartcontract.Hash160Type)) + md = NewMethodAndPrice(f.isNaturalizedCitizen, 1<<15, callflag.ReadStates) + f.AddMethod(md, desc) + + // getNaturalizationInfo method + desc = NewDescriptor("getNaturalizationInfo", smartcontract.ArrayType, + manifest.NewParameter("owner", smartcontract.Hash160Type)) + md = NewMethodAndPrice(f.getNaturalizationInfo, 1<<15, callflag.ReadStates) + f.AddMethod(md, desc) + // Events eDesc := NewEventDescriptor(VisitorRegisteredEvent, manifest.NewParameter("owner", smartcontract.Hash160Type), @@ -135,6 +189,21 @@ func newFederation() *Federation { manifest.NewParameter("remaining", smartcontract.IntegerType)) f.AddEvent(NewEvent(eDesc)) + eDesc = NewEventDescriptor(AsylumGrantedEvent, + manifest.NewParameter("owner", smartcontract.Hash160Type), + manifest.NewParameter("homeChainID", smartcontract.IntegerType), + manifest.NewParameter("reason", smartcontract.StringType)) + f.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(AsylumRevokedEvent, + manifest.NewParameter("owner", smartcontract.Hash160Type)) + f.AddEvent(NewEvent(eDesc)) + + eDesc = NewEventDescriptor(CitizenNaturalizedEvent, + manifest.NewParameter("owner", smartcontract.Hash160Type), + manifest.NewParameter("originalHomeChain", smartcontract.IntegerType)) + f.AddEvent(NewEvent(eDesc)) + return f } @@ -196,6 +265,20 @@ func makeDebtKey(chainID uint32) []byte { return key } +func makeAsylumKey(owner util.Uint160) []byte { + key := make([]byte, 1+util.Uint160Size) + key[0] = prefixAsylumRegistry + copy(key[1:], owner.BytesBE()) + return key +} + +func makeNaturalizedKey(owner util.Uint160) []byte { + key := make([]byte, 1+util.Uint160Size) + key[0] = prefixNaturalizedCitizen + copy(key[1:], owner.BytesBE()) + return key +} + // Internal storage methods func (f *Federation) getFeePercentInternal(d *dao.Simple) uint8 { @@ -250,6 +333,60 @@ func (f *Federation) addDebtInternal(d *dao.Simple, chainID uint32, amount *big. f.setDebtInternal(d, chainID, newDebt) } +// Asylum record storage format: homeChainID (4 bytes) + grantedAt (4 bytes) + reason (variable) +func (f *Federation) getAsylumInternal(d *dao.Simple, owner util.Uint160) (homeChainID uint32, grantedAt uint32, reason string, exists bool) { + si := d.GetStorageItem(f.ID, makeAsylumKey(owner)) + if si == nil || len(si) < 8 { + return 0, 0, "", false + } + homeChainID = binary.BigEndian.Uint32(si[0:4]) + grantedAt = binary.BigEndian.Uint32(si[4:8]) + if len(si) > 8 { + reason = string(si[8:]) + } + return homeChainID, grantedAt, reason, true +} + +func (f *Federation) setAsylumInternal(d *dao.Simple, owner util.Uint160, homeChainID uint32, grantedAt uint32, reason string) { + buf := make([]byte, 8+len(reason)) + binary.BigEndian.PutUint32(buf[0:4], homeChainID) + binary.BigEndian.PutUint32(buf[4:8], grantedAt) + copy(buf[8:], reason) + d.PutStorageItem(f.ID, makeAsylumKey(owner), buf) +} + +func (f *Federation) deleteAsylumInternal(d *dao.Simple, owner util.Uint160) { + d.DeleteStorageItem(f.ID, makeAsylumKey(owner)) +} + +func (f *Federation) hasAsylumInternal(d *dao.Simple, owner util.Uint160) bool { + si := d.GetStorageItem(f.ID, makeAsylumKey(owner)) + return si != nil +} + +// Naturalization record storage format: originalHomeChain (4 bytes) + naturalizedAt (4 bytes) +func (f *Federation) getNaturalizedInternal(d *dao.Simple, owner util.Uint160) (originalHomeChain uint32, naturalizedAt uint32, exists bool) { + si := d.GetStorageItem(f.ID, makeNaturalizedKey(owner)) + if si == nil || len(si) < 8 { + return 0, 0, false + } + originalHomeChain = binary.BigEndian.Uint32(si[0:4]) + naturalizedAt = binary.BigEndian.Uint32(si[4:8]) + return originalHomeChain, naturalizedAt, true +} + +func (f *Federation) setNaturalizedInternal(d *dao.Simple, owner util.Uint160, originalHomeChain uint32, naturalizedAt uint32) { + buf := make([]byte, 8) + binary.BigEndian.PutUint32(buf[0:4], originalHomeChain) + binary.BigEndian.PutUint32(buf[4:8], naturalizedAt) + d.PutStorageItem(f.ID, makeNaturalizedKey(owner), buf) +} + +func (f *Federation) isNaturalizedInternal(d *dao.Simple, owner util.Uint160) bool { + si := d.GetStorageItem(f.ID, makeNaturalizedKey(owner)) + return si != nil +} + // Contract methods func (f *Federation) getFeePercent(ic *interop.Context, _ []stackitem.Item) stackitem.Item { @@ -409,6 +546,137 @@ func (f *Federation) settleDebt(ic *interop.Context, args []stackitem.Item) stac return stackitem.NewBool(true) } +// Asylum methods + +func (f *Federation) grantAsylum(ic *interop.Context, args []stackitem.Item) stackitem.Item { + owner := toUint160(args[0]) + homeChainID := uint32(toBigInt(args[1]).Int64()) + reason := toString(args[2]) + + // Validate chain ID + if homeChainID == 0 { + panic(ErrInvalidChainID) + } + + // Check committee + if !f.NEO.CheckCommittee(ic) { + panic(ErrNotCommittee) + } + + // Check if already has asylum + if f.hasAsylumInternal(ic.DAO, owner) { + panic(ErrAsylumAlreadyGranted) + } + + // Grant asylum + f.setAsylumInternal(ic.DAO, owner, homeChainID, ic.BlockHeight(), reason) + + // Emit event + err := ic.AddNotification(f.Hash, AsylumGrantedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray(owner.BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(homeChainID))), + stackitem.NewByteArray([]byte(reason)), + })) + if err != nil { + panic(err) + } + + return stackitem.NewBool(true) +} + +func (f *Federation) revokeAsylum(ic *interop.Context, args []stackitem.Item) stackitem.Item { + owner := toUint160(args[0]) + + // Check committee + if !f.NEO.CheckCommittee(ic) { + panic(ErrNotCommittee) + } + + // Check if has asylum + if !f.hasAsylumInternal(ic.DAO, owner) { + panic(ErrAsylumNotFound) + } + + // Revoke asylum + f.deleteAsylumInternal(ic.DAO, owner) + + // Emit event + err := ic.AddNotification(f.Hash, AsylumRevokedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray(owner.BytesBE()), + })) + if err != nil { + panic(err) + } + + return stackitem.NewBool(true) +} + +func (f *Federation) hasAsylum(ic *interop.Context, args []stackitem.Item) stackitem.Item { + owner := toUint160(args[0]) + return stackitem.NewBool(f.hasAsylumInternal(ic.DAO, owner)) +} + +func (f *Federation) getAsylumInfo(ic *interop.Context, args []stackitem.Item) stackitem.Item { + owner := toUint160(args[0]) + homeChainID, grantedAt, reason, exists := f.getAsylumInternal(ic.DAO, owner) + if !exists { + return stackitem.Null{} + } + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(homeChainID))), + stackitem.NewBigInteger(big.NewInt(int64(grantedAt))), + stackitem.NewByteArray([]byte(reason)), + }) +} + +// Naturalization methods + +func (f *Federation) naturalize(ic *interop.Context, args []stackitem.Item) stackitem.Item { + owner := toUint160(args[0]) + originalHomeChain := uint32(toBigInt(args[1]).Int64()) + + // Check committee + if !f.NEO.CheckCommittee(ic) { + panic(ErrNotCommittee) + } + + // Check if already naturalized + if f.isNaturalizedInternal(ic.DAO, owner) { + panic(ErrAlreadyNaturalized) + } + + // Naturalize the citizen + f.setNaturalizedInternal(ic.DAO, owner, originalHomeChain, ic.BlockHeight()) + + // Emit event + err := ic.AddNotification(f.Hash, CitizenNaturalizedEvent, stackitem.NewArray([]stackitem.Item{ + stackitem.NewByteArray(owner.BytesBE()), + stackitem.NewBigInteger(big.NewInt(int64(originalHomeChain))), + })) + if err != nil { + panic(err) + } + + return stackitem.NewBool(true) +} + +func (f *Federation) isNaturalizedCitizen(ic *interop.Context, args []stackitem.Item) stackitem.Item { + owner := toUint160(args[0]) + return stackitem.NewBool(f.isNaturalizedInternal(ic.DAO, owner)) +} + +func (f *Federation) getNaturalizationInfo(ic *interop.Context, args []stackitem.Item) stackitem.Item { + owner := toUint160(args[0]) + originalHomeChain, naturalizedAt, exists := f.getNaturalizedInternal(ic.DAO, owner) + if !exists { + return stackitem.Null{} + } + return stackitem.NewArray([]stackitem.Item{ + stackitem.NewBigInteger(big.NewInt(int64(originalHomeChain))), + stackitem.NewBigInteger(big.NewInt(int64(naturalizedAt))), + }) +} + // Public methods for cross-native access // GetVisitingFeePercent returns the visiting fee percent (for cross-native access). @@ -442,3 +710,13 @@ func (f *Federation) GetInterChainDebt(d *dao.Simple, chainID uint32) *big.Int { func (f *Federation) Address() util.Uint160 { return f.Hash } + +// HasAsylum checks if an address has asylum status (for cross-native access). +func (f *Federation) HasAsylum(d *dao.Simple, owner util.Uint160) bool { + return f.hasAsylumInternal(d, owner) +} + +// IsNaturalizedCitizen checks if an address is a naturalized citizen (for cross-native access). +func (f *Federation) IsNaturalizedCitizen(d *dao.Simple, owner util.Uint160) bool { + return f.isNaturalizedInternal(d, owner) +} diff --git a/pkg/core/native/native_gas.go b/pkg/core/native/native_gas.go index 909054e..e27d76a 100644 --- a/pkg/core/native/native_gas.go +++ b/pkg/core/native/native_gas.go @@ -39,6 +39,9 @@ const ( VitaExemptLocal // VitaExemptVisitor indicates a visiting Vita holder (split between local and home chain). VitaExemptVisitor + // VitaExemptAsylum indicates an asylum seeker (100% local Treasury, no inter-chain debt). + // Used for humanitarian override when home chain becomes hostile. + VitaExemptAsylum ) // GASFactor is a divisor for finding GAS integral value. @@ -143,6 +146,10 @@ func (g *GAS) OnPersist(ic *interop.Context) error { homeChain = g.Federation.GetHomeChain(ic.DAO, sender) } g.burnFromTreasuryWithSplit(ic, absAmount, homeChain) + case VitaExemptAsylum: + // Asylum seeker: 100% from local Treasury, no inter-chain debt + // Humanitarian override - don't fund hostile home chain + g.burnFromTreasury(ic, absAmount) default: // Non-citizen: burn from sender g.Burn(ic, sender, absAmount) @@ -179,10 +186,23 @@ func (g *GAS) getVitaExemptType(d *dao.Simple, sender util.Uint160) VitaExemptTy return VitaExemptLocal } } + + // Check naturalized citizen (treated same as local) + if g.Federation != nil && g.Federation.IsNaturalizedCitizen(d, sender) { + return VitaExemptLocal + } + // Check visitor registry if g.Federation != nil && g.Federation.IsVisitor(d, sender) { return VitaExemptVisitor } + + // Check asylum registry (humanitarian override) + // Asylum seekers get fee exemption even if their home chain revoked their Vita + if g.Federation != nil && g.Federation.HasAsylum(d, sender) { + return VitaExemptAsylum + } + return VitaExemptNone }