Skip to content

Commit

Permalink
Enable arbitrary contract calls on /construction/metadata (#218)
Browse files Browse the repository at this point in the history
* add args encoded

* add support for arbitrary methods

* add methods for contract call data parsing

* integrate

* tests

* fix lint error

* fix validateMethodSignature function

* add test for validateMethodSignature

* add license to method_registry_test.go

* make ArgeEncoded a boolean
  • Loading branch information
gqln authored Oct 18, 2023
1 parent f9873b1 commit b5ccc81
Show file tree
Hide file tree
Showing 8 changed files with 442 additions and 21 deletions.
4 changes: 4 additions & 0 deletions airgap/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ type TxArgs struct {
// non-nil means celo registry contract invokation
Method *CeloMethod
Args []interface{}
// ArgsEncoded is set to true for arbitrary contract calls where
// the args array has one element containing the pre-encoded args data,
// as a hex encoded string
ArgsEncoded bool
}

type CallParams struct {
Expand Down
66 changes: 65 additions & 1 deletion airgap/method_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ var methodRegistry = make(map[string]map[string]*CeloMethod)
func MethodFromString(celoMethodStr string) (*CeloMethod, error) {
parts := strings.Split(celoMethodStr, ".")
if len(parts) != 2 {
return nil, fmt.Errorf("Invalid method string: %s", celoMethodStr)
// If there is no . present, try to parse this string as a generic EVM method signature
unregisteredMethod, err := unregisteredMethodFromString(celoMethodStr)
if err != nil {
return nil, err
}
return unregisteredMethod, nil
}
m, ok := methodRegistry[parts[0]]
if !ok {
Expand All @@ -52,3 +57,62 @@ func registerMethod(contract string, name string, argParsers []argParser) *CeloM
methodRegistry[contract][name] = cm
return cm
}

// unregisteredMethodFromString returns a CeloMethod representing an arbitrary EVM-compatible method
// Unknown methods are represented by their function signature string like "transfer(bytes32,address)"
func unregisteredMethodFromString(methodSignature string) (*CeloMethod, error) {
if err := validateMethodSignature(methodSignature); err != nil {
return nil, err
}
unregisteredMethod := CeloMethod{
Name: methodSignature,
// Unregistered methods do not identify contract by name
// The "to" address value should be set to the contract's address
Contract: "",
// Unregistered methods do not use argParsers
// Args will be pre-encoded OR parsed based on the methodSignature
argParsers: nil,
}
return &unregisteredMethod, nil
}

func validateMethodSignature(methodSig string) error {
// Check if the method signature contains both opening and closing parentheses
openParenIndex := strings.Index(methodSig, "(")
closeParenIndex := strings.Index(methodSig, ")")
if openParenIndex == -1 || closeParenIndex == -1 || openParenIndex > closeParenIndex {
return fmt.Errorf("Invalid method signature: %s", methodSig)
}

// Extract the contents inside the parentheses
paramString := methodSig[openParenIndex+1 : closeParenIndex]

// If there are no contents, the signature is valid
if paramString == "" {
return nil
}

// Split the contents by comma to get individual type strings
methodTypes := strings.Split(paramString, ",")

// Iterate through each type string and validate
for _, v := range methodTypes {
v = strings.TrimSpace(v) // Trim any leading/trailing whitespace
switch {
case v == "address":
continue
case strings.HasPrefix(v, "uint") || strings.HasPrefix(v, "int"):
continue
case strings.HasPrefix(v, "bytes"):
continue
case v == "string":
continue
case v == "bool":
continue
default:
return fmt.Errorf("Invalid type %s in method signature: %s", v, methodSig)
}
}

return nil
}
39 changes: 39 additions & 0 deletions airgap/method_registry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright 2020 Celo Org
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package airgap

import "testing"

func Test_validateMethodSignature(t *testing.T) {
tests := []struct {
name string
methodSig string
wantErr bool
}{
{name: "valid signature with no args", methodSig: "noArgs()", wantErr: false},
{name: "valid signature with one arg", methodSig: "deploy(address)", wantErr: false},
{name: "valid signature with multiple args", methodSig: "deploy(address,uint8,bytes16,address)", wantErr: false},
{name: "signature with invalid arg type", methodSig: "batchTransfer(DepositWalletTransfer[])", wantErr: true},
{name: "closing parenthesis only", methodSig: "noArgs)", wantErr: true},
{name: "open parenthesis only", methodSig: "noArgs(", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := validateMethodSignature(tt.methodSig); (err != nil) != tt.wantErr {
t.Errorf("validateMethodSignature() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
7 changes: 6 additions & 1 deletion airgap/methods.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,12 @@ type CeloMethod struct {
argParsers []argParser
}

func (cm *CeloMethod) String() string { return fmt.Sprintf("%s.%s", cm.Contract, cm.Name) }
func (cm *CeloMethod) String() string {
if cm.Contract == "" {
return cm.Name
}
return fmt.Sprintf("%s.%s", cm.Contract, cm.Name)
}

func (cm *CeloMethod) SerializeArguments(args ...interface{}) ([]interface{}, error) {
if len(args) != len(cm.argParsers) {
Expand Down
107 changes: 107 additions & 0 deletions airgap/server/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"testing"

"github.com/celo-org/celo-blockchain/common"
"github.com/celo-org/celo-blockchain/common/hexutil"
"github.com/celo-org/rosetta/airgap"
. "github.com/onsi/gomega"
)
Expand Down Expand Up @@ -91,4 +92,110 @@ func TestClientServer(t *testing.T) {

_ = tx
})

t.Run("contract call with no arguments", func(t *testing.T) {
RegisterTestingT(t)

txArgs := buildContractCallTxArgs(common.HexToAddress("A"), 0, common.HexToAddress("B"), "noArgs()", []interface{}{}, false)
Ω(err).ShouldNot(HaveOccurred())

txArgs, err = simulateWire(txArgs)
Ω(err).ShouldNot(HaveOccurred())

txMetadata, err := server.ObtainMetadata(ctx, txArgs)
Ω(err).ShouldNot(HaveOccurred())

hexEncodedData := hexutil.Encode(txMetadata.Data)
expectedData := "0x83c962bb" // The data is just the function selector
Ω(hexEncodedData).Should(Equal(expectedData))

tx, err := client.ConstructTxFromMetadata(txMetadata)
Ω(err).ShouldNot(HaveOccurred())

_ = tx
})

t.Run("contract call with unencoded primitive-typed arguments", func(t *testing.T) {
RegisterTestingT(t)

methodSig := "withdraw(address,uint256,uint32,bytes)"
methodArgs := []interface{}{"0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000", "23535", "0", "0x"}
txArgs := buildContractCallTxArgs(common.HexToAddress("A"), 0, common.HexToAddress("B"), methodSig, methodArgs, false)
Ω(err).ShouldNot(HaveOccurred())

txArgs, err = simulateWire(txArgs)
Ω(err).ShouldNot(HaveOccurred())

txMetadata, err := server.ObtainMetadata(ctx, txArgs)
Ω(err).ShouldNot(HaveOccurred())

hexEncodedData := hexutil.Encode(txMetadata.Data)
expectedData := "0x32b7006d000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddead00000000000000000000000000000000000000000000000000000000000000005bef000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000"
Ω(hexEncodedData).Should(Equal(expectedData))

tx, err := client.ConstructTxFromMetadata(txMetadata)
Ω(err).ShouldNot(HaveOccurred())

_ = tx
})

t.Run("contract call with fixed-length byte argument", func(t *testing.T) {
RegisterTestingT(t)

methodSig := "deploy(bytes32,address,address)"
methodArgs := []interface{}{"0x0000000000000000000000000000000000000000000000000000000000000000", "0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000", "0xb0935a466e6Fa8FDa8143C7f4a8c149CA56D06FE"}
txArgs := buildContractCallTxArgs(common.HexToAddress("A"), 0, common.HexToAddress("B"), methodSig, methodArgs, false)
Ω(err).ShouldNot(HaveOccurred())

txArgs, err = simulateWire(txArgs)
Ω(err).ShouldNot(HaveOccurred())

txMetadata, err := server.ObtainMetadata(ctx, txArgs)
Ω(err).ShouldNot(HaveOccurred())

hexEncodedData := hexutil.Encode(txMetadata.Data)
expectedData := "0xcf9d137c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddead0000000000000000000000000000b0935a466e6fa8fda8143c7f4a8c149ca56d06fe"
Ω(hexEncodedData).Should(Equal(expectedData))

tx, err := client.ConstructTxFromMetadata(txMetadata)
Ω(err).ShouldNot(HaveOccurred())

_ = tx
})

t.Run("contract call with already encoded arg data", func(t *testing.T) {
RegisterTestingT(t)

methodSig := "deploy(bytes32,address,address)"
methodArgs := []interface{}{"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddead0000000000000000000000000000b0935a466e6fa8fda8143c7f4a8c149ca56d06fe"}
txArgs := buildContractCallTxArgs(common.HexToAddress("A"), 0, common.HexToAddress("B"), methodSig, methodArgs, true)
Ω(err).ShouldNot(HaveOccurred())

txArgs, err = simulateWire(txArgs)
Ω(err).ShouldNot(HaveOccurred())

txMetadata, err := server.ObtainMetadata(ctx, txArgs)
Ω(err).ShouldNot(HaveOccurred())

hexEncodedData := hexutil.Encode(txMetadata.Data)
expectedData := "0xcf9d137c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddead0000000000000000000000000000b0935a466e6fa8fda8143c7f4a8c149ca56d06fe"
Ω(hexEncodedData).Should(Equal(expectedData))

tx, err := client.ConstructTxFromMetadata(txMetadata)
Ω(err).ShouldNot(HaveOccurred())

_ = tx
})
}

func buildContractCallTxArgs(from common.Address, value int64, to common.Address, methodSig string, args []interface{}, argsEncoded bool) *airgap.TxArgs {
bigIntValue := big.NewInt(value)
return &airgap.TxArgs{
From: from,
Value: bigIntValue,
To: &to,
Method: &airgap.CeloMethod{Name: methodSig},
Args: args,
ArgsEncoded: argsEncoded,
}
}
Loading

0 comments on commit b5ccc81

Please sign in to comment.