Skip to content

Commit

Permalink
Allow to use nested types in fourbyte
Browse files Browse the repository at this point in the history
  • Loading branch information
danhper committed Feb 16, 2022
1 parent fc8ad1b commit 0fab6cd
Show file tree
Hide file tree
Showing 2 changed files with 187 additions and 21 deletions.
158 changes: 137 additions & 21 deletions signer/fourbyte/abi.go
Expand Up @@ -20,7 +20,6 @@ import (
"bytes"
"encoding/json"
"fmt"
"regexp"
"strings"

"github.com/ethereum/go-ethereum/accounts/abi"
Expand Down Expand Up @@ -75,42 +74,159 @@ func verifySelector(selector string, calldata []byte) (*decodedCallData, error)
return parseCallData(calldata, string(abidata))
}

// selectorRegexp is used to validate that a 4byte database selector corresponds
// to a valid ABI function declaration.
//
// Note, although uppercase letters are not part of the ABI spec, this regexp
// still accepts it as the general format is valid. It will be rejected later
// by the type checker.
var selectorRegexp = regexp.MustCompile(`^([^\)]+)\(([A-Za-z0-9,\[\]]*)\)`)
func isDigit(c byte) bool {
return c >= '0' && c <= '9'
}

func isAlpha(c byte) bool {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}

func isIdentifierSymbol(c byte) bool {
return c == '$' || c == '_'
}

func parseToken(unescapedSelector string, isIdent bool) (string, string, error) {
if len(unescapedSelector) == 0 {
return "", "", fmt.Errorf("empty token")
}
firstChar := unescapedSelector[0]
position := 1
if !(isAlpha(firstChar) || (isIdent && isIdentifierSymbol(firstChar))) {
return "", "", fmt.Errorf("invalid token start: %c", firstChar)
}
for position < len(unescapedSelector) {
char := unescapedSelector[position]
if !(isAlpha(char) || isDigit(char) || (isIdent && isIdentifierSymbol(char))) {
break
}
position++
}
return unescapedSelector[:position], unescapedSelector[position:], nil
}

func parseIdentifier(unescapedSelector string) (string, string, error) {
return parseToken(unescapedSelector, true)
}

func parseElementaryType(unescapedSelector string) (string, string, error) {
parsedType, rest, err := parseToken(unescapedSelector, false)
if err != nil {
return "", "", fmt.Errorf("failed to parse elementary type: %v", err)
}
// handle arrays
for len(rest) > 0 && rest[0] == '[' {
parsedType = parsedType + string(rest[0])
rest = rest[1:]
for len(rest) > 0 && isDigit(rest[0]) {
parsedType = parsedType + string(rest[0])
rest = rest[1:]
}
if len(rest) == 0 || rest[0] != ']' {
return "", "", fmt.Errorf("failed to parse array: expected ']', got %c", unescapedSelector[0])
}
parsedType = parsedType + string(rest[0])
rest = rest[1:]
}
return parsedType, rest, nil
}

func parseCompositeType(unescapedSelector string) ([]interface{}, string, error) {
if len(unescapedSelector) == 0 || unescapedSelector[0] != '(' {
return nil, "", fmt.Errorf("expected '(', got %c", unescapedSelector[0])
}
parsedType, rest, err := parseType(unescapedSelector[1:])
if err != nil {
return nil, "", fmt.Errorf("failed to parse type: %v", err)
}
result := []interface{}{parsedType}
for len(rest) > 0 && rest[0] != ')' {
parsedType, rest, err = parseType(rest[1:])
if err != nil {
return nil, "", fmt.Errorf("failed to parse type: %v", err)
}
result = append(result, parsedType)
}
if len(rest) == 0 || rest[0] != ')' {
return nil, "", fmt.Errorf("expected ')', got '%s'", rest)
}
return result, rest[1:], nil
}

func parseType(unescapedSelector string) (interface{}, string, error) {
if len(unescapedSelector) == 0 {
return nil, "", fmt.Errorf("empty type")
}
if unescapedSelector[0] == '(' {
return parseCompositeType(unescapedSelector)
} else {
return parseElementaryType(unescapedSelector)
}
}

// parseSelector converts a method selector into an ABI JSON spec. The returned
// data is a valid JSON string which can be consumed by the standard abi package.
// Note, although uppercase letters are not part of the ABI spec, this function
// still accepts it as the general format is valid. It will be rejected later
// by the type checker.
func parseSelector(unescapedSelector string) ([]byte, error) {
// Define a tiny fake ABI struct for JSON marshalling
type fakeArg struct {
Type string `json:"type"`
Name string `json:"name"`
Type string `json:"type"`
Components []fakeArg `json:"components"`
}
type fakeABI struct {
Name string `json:"name"`
Type string `json:"type"`
Inputs []fakeArg `json:"inputs"`
}
// Validate the unescapedSelector and extract it's components
groups := selectorRegexp.FindStringSubmatch(unescapedSelector)
if len(groups) != 3 {
return nil, fmt.Errorf("invalid selector %q (%v matches)", unescapedSelector, len(groups))

name, rest, err := parseIdentifier(unescapedSelector)
if err != nil {
return nil, fmt.Errorf("failed to parse selector '%s': %v", unescapedSelector, err)
}
args := []interface{}{}
if len(rest) >= 2 && rest[0] == '(' && rest[1] == ')' {
rest = rest[2:]
} else {
args, rest, err = parseCompositeType(rest)
if err != nil {
return nil, fmt.Errorf("failed to parse selector '%s': %v", unescapedSelector, err)
}
}
if len(rest) > 0 {
return nil, fmt.Errorf("failed to parse selector '%s': unexpected string '%s'", unescapedSelector, rest)
}
name := groups[1]
args := groups[2]

// Reassemble the fake ABI and constuct the JSON
arguments := make([]fakeArg, 0)
if len(args) > 0 {
for _, arg := range strings.Split(args, ",") {
arguments = append(arguments, fakeArg{arg})
var assembleArgs func([]interface{}) ([]fakeArg, error)
assembleArgs = func(args []interface{}) ([]fakeArg, error) {
arguments := make([]fakeArg, 0)
for i, arg := range args {
// generate dummy name to avoid unmarshal issues
name := fmt.Sprintf("name%d", i)
if s, ok := arg.(string); ok {
arguments = append(arguments, fakeArg{name, s, nil})
} else if components, ok := arg.([]interface{}); ok {
subArgs, err := assembleArgs(components)
if err != nil {
return nil, fmt.Errorf("failed to assemble components: %v", err)
}
arguments = append(arguments, fakeArg{name, "tuple", subArgs})
} else {
return nil, fmt.Errorf("failed to assemble args: unexpected type %T", arg)
}
}
return arguments, nil
}

// Reassemble the fake ABI and constuct the JSON
fakeArgs, err := assembleArgs(args)
if err != nil {
return nil, fmt.Errorf("failed to parse selector: %v", err)
}
return json.Marshal([]fakeABI{{name, "function", arguments}})

return json.Marshal([]fakeABI{{name, "function", fakeArgs}})
}

// parseCallData matches the provided call data against the ABI definition and
Expand Down
50 changes: 50 additions & 0 deletions signer/fourbyte/abi_test.go
Expand Up @@ -17,6 +17,8 @@
package fourbyte

import (
"encoding/json"
"fmt"
"math/big"
"reflect"
"strings"
Expand Down Expand Up @@ -172,3 +174,51 @@ func TestMaliciousABIStrings(t *testing.T) {
}
}
}

func TestParseSelector(t *testing.T) {
mkType := func(types ...interface{}) []interface{} {
var result []interface{}
for i, typeOrComponents := range types {
name := fmt.Sprintf("name%d", i)
if typeName, ok := typeOrComponents.(string); ok {
result = append(result, map[string]interface{}{"name": name, "type": typeName, "components": nil})
} else {
result = append(result, map[string]interface{}{"name": name, "type": "tuple", "components": typeOrComponents})
}
}
return result
}
tests := []struct {
input string
name string
args []interface{}
}{
{"noargs()", "noargs", []interface{}{}},
{"simple(uint256,uint256,uint256)", "simple", mkType("uint256", "uint256", "uint256")},
{"other(uint256,address)", "other", mkType("uint256", "address")},
{"withArray(uint256[],address[2],uint8[4][][5])", "withArray", mkType("uint256[]", "address[2]", "uint8[4][][5]")},
{"singleNest(bytes32,uint8,(uint256,uint256),address)", "singleNest", mkType("bytes32", "uint8", mkType("uint256", "uint256"), "address")},
{"multiNest(address,(uint256[],uint256),((address,bytes32),uint256))", "multiNest",
mkType("address", mkType("uint256[]", "uint256"), mkType(mkType("address", "bytes32"), "uint256"))},
}
var parsed []map[string]interface{}
for i, tt := range tests {
encoded, err := parseSelector(tt.input)
if err != nil {
t.Errorf("test %d: failed to parse selector '%v': %v", i, tt.input, err)
}
err = json.Unmarshal(encoded, &parsed)
if err != nil || len(parsed) != 1 {
t.Errorf("test %d: failed to decode selector '%v' ('%v'): %v", i, tt.input, encoded, err)
}
abiFunc := parsed[0]
if name, ok := abiFunc["name"]; !ok || name != tt.name {
t.Errorf("test %d: unexpected function name: '%s' != '%s'", i, name, tt.name)
}

inputs := abiFunc["inputs"].([]interface{})
if !reflect.DeepEqual(inputs, tt.args) {
t.Errorf("test %d: unexpected args: '%v' != '%v'", i, inputs, tt.args)
}
}
}

0 comments on commit 0fab6cd

Please sign in to comment.