Skip to content

Commit

Permalink
nns: Prevent users from accessing top-level domains
Browse files Browse the repository at this point in the history
Top-level domains aren't NFTs, therefore NNS contract must not treat
them as such. At the same time, technically TLDs are valid domains (e.g.
'org'), so nothing will prevent the user from performing operations with
them. Based on this, the best approach would be treating TLDs as
non-existent domains.

Throw 'token not found' exception on TLD input of any method. The
storage model is left the same: this allows us not to perform migration
and implement the behavior logically.

Refs 334.

Signed-off-by: Leonard Lyubich <leonard@morphbits.io>
  • Loading branch information
cthulhu-rider committed Jun 10, 2023
1 parent 1d68511 commit 5758c84
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 38 deletions.
84 changes: 79 additions & 5 deletions nns/nns_contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,15 +137,29 @@ func TotalSupply() int {
return getTotalSupply(ctx)
}

// OwnerOf returns the owner of the specified domain.
// OwnerOf returns the owner of the specified domain. The tokenID domain MUST
// NOT be a TLD.
func OwnerOf(tokenID []byte) interop.Hash160 {
// TODO: same done in getNameState, don't do twice
fragments := std.StringSplit(string(tokenID), ".")
if len(fragments) == 1 {
panic("token not found")
}

ctx := storage.GetReadOnlyContext()
ns := getNameState(ctx, tokenID)
return ns.Owner
}

// Properties returns a domain name and an expiration date of the specified domain.
// The tokenID MUST NOT be a TLD.
func Properties(tokenID []byte) map[string]interface{} {
// TODO: same done in getNameState, don't do twice
fragments := std.StringSplit(string(tokenID), ".")
if len(fragments) == 1 {
panic("token not found")
}

ctx := storage.GetReadOnlyContext()
ns := getNameState(ctx, tokenID)
return map[string]interface{}{
Expand Down Expand Up @@ -183,10 +197,18 @@ func TokensOf(owner interop.Hash160) iterator.Iterator {
}

// Transfer transfers the domain with the specified name to a new owner.
// The tokenID MUST NOT be a TLD.
func Transfer(to interop.Hash160, tokenID []byte, data interface{}) bool {
if !isValid(to) {
panic(`invalid receiver`)
}

// TODO: same done in getNameState, don't do twice
fragments := std.StringSplit(string(tokenID), ".")
if len(fragments) == 1 {
panic("token not found")
}

var (
tokenKey = getTokenKey(tokenID)
ctx = storage.GetContext()
Expand Down Expand Up @@ -234,7 +256,8 @@ func GetPrice() int {
return storage.Get(ctx, []byte{prefixRegisterPrice}).(int)
}

// IsAvailable checks whether the provided domain name is available.
// IsAvailable checks whether the provided domain name is available. Notice that
// TLD is available for the committee only.
func IsAvailable(name string) bool {
fragments := splitAndCheck(name, false)
if fragments == nil {
Expand Down Expand Up @@ -405,11 +428,18 @@ func UpdateSOA(name, email string, refresh, retry, expire, ttl int) {
putSoaRecord(ctx, name, email, refresh, retry, expire, ttl)
}

// SetAdmin updates domain admin.
// SetAdmin updates domain admin. The name MUST NOT be a TLD.
func SetAdmin(name string, admin interop.Hash160) {
if len(name) > maxDomainNameLength {
panic("invalid domain name format")
}

// TODO: same done in getNameState, don't do twice
fragments := std.StringSplit(name, ".")
if len(fragments) == 1 {
panic("token not found")
}

if admin != nil && !runtime.CheckWitness(admin) {
panic("not witnessed by admin")
}
Expand All @@ -421,11 +451,19 @@ func SetAdmin(name string, admin interop.Hash160) {
}

// SetRecord adds a new record of the specified type to the provided domain.
// The name MUST NOT be a TLD.
func SetRecord(name string, typ RecordType, id byte, data string) {
tokenID := []byte(tokenIDFromName(name))
if !checkBaseRecords(typ, data) {
panic("invalid record data")
}

// TODO: same done in getNameState, don't do twice
fragments := std.StringSplit(name, ".")
if len(fragments) == 1 {
panic("token not found")
}

ctx := storage.GetContext()
ns := getNameState(ctx, tokenID)
ns.checkAdmin()
Expand All @@ -449,11 +487,19 @@ func checkBaseRecords(typ RecordType, data string) bool {
}

// AddRecord adds a new record of the specified type to the provided domain.
// The name MUST NOT be a TLD.
func AddRecord(name string, typ RecordType, data string) {
tokenID := []byte(tokenIDFromName(name))
if !checkBaseRecords(typ, data) {
panic("invalid record data")
}

// TODO: same done in getNameState, don't do twice
fragments := std.StringSplit(string(tokenID), ".")
if len(fragments) == 1 {
panic("token not found")
}

ctx := storage.GetContext()
ns := getNameState(ctx, tokenID)
ns.checkAdmin()
Expand All @@ -462,19 +508,33 @@ func AddRecord(name string, typ RecordType, data string) {
}

// GetRecords returns domain record of the specified type if it exists or an empty
// string if not.
// string if not. The name MUST NOT be a TLD.
func GetRecords(name string, typ RecordType) []string {
// TODO: same done in getNameState, don't do twice
fragments := std.StringSplit(name, ".")
if len(fragments) == 1 {
panic("token not found")
}

tokenID := []byte(tokenIDFromName(name))
ctx := storage.GetReadOnlyContext()
_ = getNameState(ctx, tokenID) // ensure not expired
return getRecordsByType(ctx, tokenID, name, typ)
}

// DeleteRecords removes domain records with the specified type.
// DeleteRecords removes domain records with the specified type. The name MUST
// NOT be a TLD.
func DeleteRecords(name string, typ RecordType) {
if typ == SOA {
panic("you cannot delete soa record")
}

// TODO: same done in getNameState, don't do twice
fragments := std.StringSplit(name, ".")
if len(fragments) == 1 {
panic("token not found")
}

tokenID := []byte(tokenIDFromName(name))
ctx := storage.GetContext()
ns := getNameState(ctx, tokenID)
Expand All @@ -489,13 +549,27 @@ func DeleteRecords(name string, typ RecordType) {
}

// Resolve resolves given name (not more then three redirects are allowed).
// The name MUST NOT be a TLD.
func Resolve(name string, typ RecordType) []string {
// TODO: same done in getNameState, don't do twice
fragments := std.StringSplit(name, ".")
if len(fragments) == 1 {
panic("token not found")
}

ctx := storage.GetReadOnlyContext()
return resolve(ctx, nil, name, typ, 2)
}

// GetAllRecords returns an Iterator with RecordState items for the given name.
// The name MUST NOT be a TLD.
func GetAllRecords(name string) iterator.Iterator {
// TODO: same done in getNameState, don't do twice
fragments := std.StringSplit(name, ".")
if len(fragments) == 1 {
panic("token not found")
}

tokenID := []byte(tokenIDFromName(name))
ctx := storage.GetReadOnlyContext()
_ = getNameState(ctx, tokenID) // ensure not expired
Expand Down
60 changes: 27 additions & 33 deletions tests/nns_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,15 +133,6 @@ func TestNNSRegister(t *testing.T) {
c.Invoke(t, expected, "getRecords", "testdomain.com", int64(nns.TXT))
}

func TestTLDRecord(t *testing.T) {
c := newNNSInvoker(t, true)
c.Invoke(t, stackitem.Null{}, "addRecord",
"com", int64(nns.A), "1.2.3.4")

result := []stackitem.Item{stackitem.NewByteArray([]byte("1.2.3.4"))}
c.Invoke(t, result, "resolve", "com", int64(nns.A))
}

func TestNNSRegisterMulti(t *testing.T) {
c := newNNSInvoker(t, true)

Expand Down Expand Up @@ -431,33 +422,36 @@ func TestNNSRegisterAccess(t *testing.T) {
}

func TestPredefinedTLD(t *testing.T) {
const anyTLD1 = "hello"
const anyTLD2 = "world"
predefined := []string{"hello", "world"}
const otherTLD = "goodbye"

inv := newNNSInvoker(t, false, anyTLD1, anyTLD2)
inv := newNNSInvoker(t, false, predefined...)

require.Nil(t, getDomainOwner(t, inv, anyTLD1))
require.Nil(t, getDomainOwner(t, inv, anyTLD2))
}
inv.Invoke(t, true, "isAvailable", otherTLD)

// getDomainOwner reads owner of the domain. Returns nil if domain is owned by the committee.
func getDomainOwner(tb testing.TB, inv *neotest.ContractInvoker, domain string) *util.Uint160 {
stack, err := inv.TestInvoke(tb, "ownerOf", domain)
require.NoError(tb, err)

arr := stack.ToArray()
require.Len(tb, arr, 1)

item := arr[0]
if _, ok := item.(stackitem.Null); ok {
return nil
for i := range predefined {
inv.Invoke(t, false, "isAvailable", predefined[i])
}
}

b, err := item.TryBytes()
require.NoError(tb, err)

res, err := util.Uint160DecodeBytesBE(b)
require.NoError(tb, err)

return &res
func TestNNSTLD(t *testing.T) {
const tld = "any-tld"
const tldFailMsg = "token not found"
const recTyp = int64(nns.TXT) // InvokeFail doesn't support nns.RecordType

inv := newNNSInvoker(t, false, tld)

inv.InvokeFail(t, tldFailMsg, "addRecord", tld, recTyp, "any data")
inv.InvokeFail(t, tldFailMsg, "deleteRecords", tld, recTyp)
inv.InvokeFail(t, tldFailMsg, "getAllRecords", tld)
inv.InvokeFail(t, tldFailMsg, "getRecords", tld, recTyp)
inv.Invoke(t, false, "isAvailable", tld)
inv.InvokeFail(t, tldFailMsg, "ownerOf", tld)
inv.InvokeFail(t, tldFailMsg, "properties", tld)
inv.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) {}, "renew", tld)
inv.InvokeFail(t, tldFailMsg, "resolve", tld, recTyp)
inv.InvokeFail(t, tldFailMsg, "setAdmin", tld, util.Uint160{})
inv.InvokeFail(t, tldFailMsg, "setRecord", tld, recTyp, 1, "any data")
inv.InvokeFail(t, tldFailMsg, "transfer", util.Uint160{}, tld, nil)
inv.Invoke(t, stackitem.Null{}, "updateSOA", tld, "user@domain.org", 0, 1, 2, 3)
}

0 comments on commit 5758c84

Please sign in to comment.