Skip to content

Commit

Permalink
Use special strings for methodSig to handle contract call by pre-comp…
Browse files Browse the repository at this point in the history
…iled ABI data (#123)

* Handle method passed in as hexdecimal string

* Handle args as an interface array

* Preprocess args

* Use special strings for methodSig to call fallback contract

* Simplify preprocessArgs()
  • Loading branch information
songge-cb authored Jul 19, 2024
1 parent 30a5caf commit 0e198c3
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 2 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ go 1.20
require (
github.com/coinbase/rosetta-sdk-go v0.8.2
github.com/ethereum/go-ethereum v1.13.8
github.com/holiman/uint256 v1.2.4
github.com/neilotoole/errgroup v0.1.6
github.com/stretchr/testify v1.8.4
golang.org/x/crypto v0.17.0
Expand Down Expand Up @@ -49,6 +48,7 @@ require (
github.com/gorilla/mux v1.8.0 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/holiman/bloomfilter/v2 v2.0.3 // indirect
github.com/holiman/uint256 v1.2.4 // indirect
github.com/huin/goupnp v1.3.0 // indirect
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
github.com/klauspost/compress v1.15.15 // indirect
Expand Down
38 changes: 37 additions & 1 deletion services/construction/contract_call_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import (
"golang.org/x/crypto/sha3"
)

const NoMethodSig = "NO-METHOD-SIG"

// ConstructContractCallDataGeneric constructs the data field of a transaction.
// The methodArgs can be already in ABI encoded format in case of a single string
// It can also be passed in as a slice of args, which requires further encoding.
Expand All @@ -38,8 +40,14 @@ func ConstructContractCallDataGeneric(methodSig string, methodArgs interface{})
return nil, err
}

// preprocess method args for fallback pattern contract call
args, err := preprocessArgs(methodSig, methodArgs)
if err != nil {
return nil, err
}

// switch on the type of the method args. method args can come in from json as either a string or list of strings
switch methodArgs := methodArgs.(type) {
switch methodArgs := args.(type) {
// case 0: no method arguments, return the selector
case nil:
return data, nil
Expand All @@ -65,6 +73,7 @@ func ConstructContractCallDataGeneric(methodSig string, methodArgs interface{})
}
strList = append(strList, strVal)
}

return encodeMethodArgsStrings(data, methodSig, strList)

// case 3: method args are encoded as a list of strings, which will be decoded
Expand All @@ -80,6 +89,28 @@ func ConstructContractCallDataGeneric(methodSig string, methodArgs interface{})
}
}

// preprocessArgs converts methodArgs to a string value if methodSig is an empty string.
// We are calling a contract written with fallback pattern, which has no method signature.
func preprocessArgs(methodSig string, methodArgs interface{}) (interface{}, error) {
if methodSig == "" || methodSig == NoMethodSig {
switch args := methodArgs.(type) {
case []interface{}:
if len(args) == 1 {
if argStr, ok := args[0].(string); ok {
return argStr, nil
}
return nil, fmt.Errorf("failed to convert method arg \"%T\" to string", args[0])
}
case []string:
if len(args) == 1 {
return args[0], nil
}
}
}

return methodArgs, nil
}

// encodeMethodArgsStrings constructs the data field of a transaction for a list of string args.
// It attempts to first convert the string arg to it's corresponding type in the method signature,
// and then performs abi encoding to the converted args list and construct the data.
Expand Down Expand Up @@ -185,6 +216,11 @@ func encodeMethodArgsStrings(methodID []byte, methodSig string, methodArgs []str
// contractCallMethodID calculates the first 4 bytes of the method
// signature for function call on contract
func contractCallMethodID(methodSig string) ([]byte, error) {
if methodSig == "" || methodSig == NoMethodSig {
// contract call without method signature (fallback pattern)
return []byte{}, nil
}

fnSignature := []byte(methodSig)
hash := sha3.NewLegacyKeccak256()
if _, err := hash.Write(fnSignature); err != nil {
Expand Down
70 changes: 70 additions & 0 deletions services/construction/contract_call_data_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ func TestConstruction_ContractCallData(t *testing.T) {
methodArgs: []interface{}{"bool abc", "0x0000000000000000000000000000000000000000", "true"},
expectedResponse: "0x60d7a2780000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000008626f6f6c20616263000000000000000000000000000000000000000000000000",
},
"happy path: method sig is an empty string and args is a list of interface": {
methodSig: "",
methodArgs: []interface{}{"0xabcde12345"},
expectedResponse: "0xabcde12345",
},
"happy path: method sig is NO-METHOD-SIG and args is a list of interface": {
methodSig: NoMethodSig,
methodArgs: []interface{}{"0xaabbcc112233"},
expectedResponse: "0xaabbcc112233",
},
"error: case string: invalid method args hex data": {
methodSig: "attest((bytes32,(address,uint64,bool,bytes32,bytes,uint256)))",
methodArgs: "!!!",
Expand All @@ -75,3 +85,63 @@ func TestConstruction_ContractCallData(t *testing.T) {
})
}
}

func TestConstruction_preprocessArgs(t *testing.T) {
tests := map[string]struct {
methodSig string
methodArgs interface{}

expectedResponse interface{}
expectedError error
}{
"happy path: method sig is function name": {
methodSig: "withdraw(address,uint256,uint32,bytes)",
methodArgs: []interface{}{
"0x2Ae3F1Ec7F1F5012CFEab0185bfc7aa3cf0DEc22",
"32941055343948244352",
"0",
"0x"},
expectedResponse: []interface{}{
"0x2Ae3F1Ec7F1F5012CFEab0185bfc7aa3cf0DEc22",
"32941055343948244352",
"0",
"0x"},
},
"happy path: method sig is empty and args is nil": {
methodSig: "",
methodArgs: nil,
expectedResponse: nil,
},
"happy path: method sig is NO-METHOD-SIG and args is a single string": {
methodSig: NoMethodSig,
methodArgs: "0x12345",
expectedResponse: "0x12345",
},
"happy path: method sig is empty and args is a list of interface": {
methodSig: "",
methodArgs: []interface{}{"0xabcde"},
expectedResponse: "0xabcde",
},
"happy path: method sig is NO-METHOD-SIG and args is a list of strings": {
methodSig: NoMethodSig,
methodArgs: []string{"0x1a2b3c"},
expectedResponse: "0x1a2b3c",
},
"unhappy path: args is a list of interface and cannot be converted to strings": {
methodSig: "",
methodArgs: []interface{}{34567},
expectedError: errors.New("failed to convert method arg \"int\" to string"),
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
argsReturned, err := preprocessArgs(test.methodSig, test.methodArgs)
if err != nil {
assert.EqualError(t, err, test.expectedError.Error())
} else {
assert.Equal(t, test.expectedResponse, argsReturned)
}
})
}
}

0 comments on commit 0e198c3

Please sign in to comment.