From 279b769fa330dd1f2aae37cd28e2b78af9784e77 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Mon, 10 Aug 2020 19:17:14 +0300 Subject: [PATCH] manifest: support interface checking There are some standards (NEP5, etc.) which impose some restrictions on what methods and events a contract must contain and their signatures. This commit supports checking if arbitrary manifest complies with the standard. --- pkg/smartcontract/manifest/manifest.go | 10 ++ pkg/smartcontract/manifest/standard/comply.go | 76 ++++++++++++ .../manifest/standard/comply_test.go | 113 ++++++++++++++++++ pkg/smartcontract/manifest/standard/doc.go | 5 + pkg/smartcontract/manifest/standard/nep17.go | 57 +++++++++ 5 files changed, 261 insertions(+) create mode 100644 pkg/smartcontract/manifest/standard/comply.go create mode 100644 pkg/smartcontract/manifest/standard/comply_test.go create mode 100644 pkg/smartcontract/manifest/standard/doc.go create mode 100644 pkg/smartcontract/manifest/standard/nep17.go diff --git a/pkg/smartcontract/manifest/manifest.go b/pkg/smartcontract/manifest/manifest.go index 89de8dbd4..f4f8ad636 100644 --- a/pkg/smartcontract/manifest/manifest.go +++ b/pkg/smartcontract/manifest/manifest.go @@ -89,6 +89,16 @@ func (a *ABI) GetMethod(name string) *Method { return nil } +// GetEvent returns event with the specified name. +func (a *ABI) GetEvent(name string) *Event { + for i := range a.Events { + if a.Events[i].Name == name { + return &a.Events[i] + } + } + return nil +} + // CanCall returns true is current contract is allowed to call // method of another contract. func (m *Manifest) CanCall(toCall *Manifest, method string) bool { diff --git a/pkg/smartcontract/manifest/standard/comply.go b/pkg/smartcontract/manifest/standard/comply.go new file mode 100644 index 000000000..afd6e4000 --- /dev/null +++ b/pkg/smartcontract/manifest/standard/comply.go @@ -0,0 +1,76 @@ +package standard + +import ( + "errors" + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" +) + +// Various validation errors. +var ( + ErrMethodMissing = errors.New("method missing") + ErrEventMissing = errors.New("event missing") + ErrInvalidReturnType = errors.New("invalid return type") + ErrInvalidParameterCount = errors.New("invalid parameter count") + ErrInvalidParameterType = errors.New("invalid parameter type") +) + +var checks = map[string]*manifest.Manifest{ + manifest.NEP17StandardName: nep17, +} + +// Check checks if manifest complies with all provided standards. +// Currently only NEP-17 is supported. +func Check(m *manifest.Manifest, standards ...string) error { + for i := range standards { + s, ok := checks[standards[i]] + if ok { + if err := Comply(m, s); err != nil { + return fmt.Errorf("manifest is not compliant with '%s': %w", standards[i], err) + } + } + } + return nil +} + +// Comply if m has all methods and event from st manifest and they have the same signature. +// Parameter names are ignored. +func Comply(m, st *manifest.Manifest) error { + for _, stm := range st.ABI.Methods { + name := stm.Name + md := m.ABI.GetMethod(name) + if md == nil { + return fmt.Errorf("%w: '%s'", ErrMethodMissing, name) + } else if stm.ReturnType != md.ReturnType { + return fmt.Errorf("%w: '%s' (expected %s, got %s)", ErrInvalidReturnType, + name, stm.ReturnType, md.ReturnType) + } else if len(stm.Parameters) != len(md.Parameters) { + return fmt.Errorf("%w: '%s' (expected %d, got %d)", ErrInvalidParameterCount, + name, len(stm.Parameters), len(md.Parameters)) + } + for i := range stm.Parameters { + if stm.Parameters[i].Type != md.Parameters[i].Type { + return fmt.Errorf("%w: '%s'[%d] (expected %s, got %s)", ErrInvalidParameterType, + name, i, stm.Parameters[i].Type, md.Parameters[i].Type) + } + } + } + for _, ste := range st.ABI.Events { + name := ste.Name + ed := m.ABI.GetEvent(name) + if ed == nil { + return fmt.Errorf("%w: event '%s'", ErrEventMissing, name) + } else if len(ste.Parameters) != len(ed.Parameters) { + return fmt.Errorf("%w: event '%s' (expected %d, got %d)", ErrInvalidParameterCount, + name, len(ste.Parameters), len(ed.Parameters)) + } + for i := range ste.Parameters { + if ste.Parameters[i].Type != ed.Parameters[i].Type { + return fmt.Errorf("%w: event '%s' (expected %s, got %s)", ErrInvalidParameterType, + name, ste.Parameters[i].Type, ed.Parameters[i].Type) + } + } + } + return nil +} diff --git a/pkg/smartcontract/manifest/standard/comply_test.go b/pkg/smartcontract/manifest/standard/comply_test.go new file mode 100644 index 000000000..1fd5b8caf --- /dev/null +++ b/pkg/smartcontract/manifest/standard/comply_test.go @@ -0,0 +1,113 @@ +package standard + +import ( + "errors" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/stretchr/testify/require" +) + +func fooMethodBarEvent() *manifest.Manifest { + return &manifest.Manifest{ + ABI: manifest.ABI{ + Methods: []manifest.Method{ + { + Name: "foo", + Parameters: []manifest.Parameter{ + {Type: smartcontract.ByteArrayType}, + {Type: smartcontract.PublicKeyType}, + }, + ReturnType: smartcontract.IntegerType, + }, + }, + Events: []manifest.Event{ + { + Name: "bar", + Parameters: []manifest.Parameter{ + {Type: smartcontract.StringType}, + }, + }, + }, + }, + } +} + +func TestComplyMissingMethod(t *testing.T) { + m := fooMethodBarEvent() + m.ABI.GetMethod("foo").Name = "notafoo" + err := Comply(m, fooMethodBarEvent()) + require.True(t, errors.Is(err, ErrMethodMissing)) +} + +func TestComplyInvalidReturnType(t *testing.T) { + m := fooMethodBarEvent() + m.ABI.GetMethod("foo").ReturnType = smartcontract.VoidType + err := Comply(m, fooMethodBarEvent()) + require.True(t, errors.Is(err, ErrInvalidReturnType)) +} + +func TestComplyMethodParameterCount(t *testing.T) { + t.Run("Method", func(t *testing.T) { + m := fooMethodBarEvent() + f := m.ABI.GetMethod("foo") + f.Parameters = append(f.Parameters, manifest.Parameter{Type: smartcontract.BoolType}) + err := Comply(m, fooMethodBarEvent()) + require.True(t, errors.Is(err, ErrInvalidParameterCount)) + }) + t.Run("Event", func(t *testing.T) { + m := fooMethodBarEvent() + ev := m.ABI.GetEvent("bar") + ev.Parameters = append(ev.Parameters[:0]) + err := Comply(m, fooMethodBarEvent()) + require.True(t, errors.Is(err, ErrInvalidParameterCount)) + }) +} + +func TestComplyParameterType(t *testing.T) { + t.Run("Method", func(t *testing.T) { + m := fooMethodBarEvent() + m.ABI.GetMethod("foo").Parameters[0].Type = smartcontract.InteropInterfaceType + err := Comply(m, fooMethodBarEvent()) + require.True(t, errors.Is(err, ErrInvalidParameterType)) + }) + t.Run("Event", func(t *testing.T) { + m := fooMethodBarEvent() + m.ABI.GetEvent("bar").Parameters[0].Type = smartcontract.InteropInterfaceType + err := Comply(m, fooMethodBarEvent()) + require.True(t, errors.Is(err, ErrInvalidParameterType)) + }) +} + +func TestMissingEvent(t *testing.T) { + m := fooMethodBarEvent() + m.ABI.GetEvent("bar").Name = "notabar" + err := Comply(m, fooMethodBarEvent()) + require.True(t, errors.Is(err, ErrEventMissing)) +} + +func TestComplyValid(t *testing.T) { + m := fooMethodBarEvent() + m.ABI.Methods = append(m.ABI.Methods, manifest.Method{ + Name: "newmethod", + Offset: 123, + ReturnType: smartcontract.ByteArrayType, + }) + m.ABI.Events = append(m.ABI.Events, manifest.Event{ + Name: "otherevent", + Parameters: []manifest.Parameter{{ + Name: "names do not matter", + Type: smartcontract.IntegerType, + }}, + }) + require.NoError(t, Comply(m, fooMethodBarEvent())) +} + +func TestCheck(t *testing.T) { + m := manifest.NewManifest(util.Uint160{}, "Test") + require.Error(t, Check(m, manifest.NEP17StandardName)) + + require.NoError(t, Check(nep17, manifest.NEP17StandardName)) +} diff --git a/pkg/smartcontract/manifest/standard/doc.go b/pkg/smartcontract/manifest/standard/doc.go new file mode 100644 index 000000000..9b22b93ac --- /dev/null +++ b/pkg/smartcontract/manifest/standard/doc.go @@ -0,0 +1,5 @@ +/* +Package standard contains interfaces for well-defined standards +and function for checking if arbitrary manifest complies with them. +*/ +package standard diff --git a/pkg/smartcontract/manifest/standard/nep17.go b/pkg/smartcontract/manifest/standard/nep17.go new file mode 100644 index 000000000..ff76c4199 --- /dev/null +++ b/pkg/smartcontract/manifest/standard/nep17.go @@ -0,0 +1,57 @@ +package standard + +import ( + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" +) + +var nep17 = &manifest.Manifest{ + ABI: manifest.ABI{ + Methods: []manifest.Method{ + { + Name: "balanceOf", + Parameters: []manifest.Parameter{ + {Type: smartcontract.Hash160Type}, + }, + ReturnType: smartcontract.IntegerType, + }, + { + Name: "decimals", + ReturnType: smartcontract.IntegerType, + }, + { + Name: "symbol", + ReturnType: smartcontract.StringType, + }, + { + Name: "totalSupply", + ReturnType: smartcontract.IntegerType, + }, + { + Name: "transfer", + Parameters: []manifest.Parameter{ + {Type: smartcontract.Hash160Type}, + {Type: smartcontract.Hash160Type}, + {Type: smartcontract.IntegerType}, + {Type: smartcontract.AnyType}, + }, + ReturnType: smartcontract.BoolType, + }, + }, + Events: []manifest.Event{ + { + Name: "Transfer", + Parameters: []manifest.Parameter{ + {Type: smartcontract.Hash160Type}, + {Type: smartcontract.Hash160Type}, + {Type: smartcontract.IntegerType}, + }, + }, + }, + }, +} + +// IsNEP17 checks if m is NEP-17 compliant. +func IsNEP17(m *manifest.Manifest) error { + return Comply(m, nep17) +}