Skip to content

Commit

Permalink
add EIP-712 package to repo
Browse files Browse the repository at this point in the history
  • Loading branch information
MalteHerrmann committed Jul 31, 2024
1 parent cfa87f2 commit 48ae68c
Show file tree
Hide file tree
Showing 13 changed files with 4,660 additions and 0 deletions.
21 changes: 21 additions & 0 deletions ethereum/eip712/domain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright Tharsis Labs Ltd.(Evmos)
// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE)
package eip712

import (
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/signer/core/apitypes"
)

// createEIP712Domain creates the typed data domain for the given chainID.
func createEIP712Domain(chainID uint64) apitypes.TypedDataDomain {
domain := apitypes.TypedDataDomain{
Name: "Cosmos Web3",
Version: "1.0.0",
ChainId: math.NewHexOrDecimal256(int64(chainID)), // #nosec G701
VerifyingContract: "cosmos",
Salt: "0",
}

return domain
}
36 changes: 36 additions & 0 deletions ethereum/eip712/eip712.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright Tharsis Labs Ltd.(Evmos)
// SPDX-License-Identifier:ENCL-1.0(https://github.com/evmos/evmos/blob/main/LICENSE)
package eip712

import (
"github.com/ethereum/go-ethereum/signer/core/apitypes"
)

// WrapTxToTypedData wraps an Amino-encoded Cosmos Tx JSON SignDoc
// bytestream into an EIP712-compatible TypedData request.
func WrapTxToTypedData(
chainID uint64,
data []byte,
) (apitypes.TypedData, error) {
messagePayload, err := createEIP712MessagePayload(data)
message := messagePayload.message
if err != nil {
return apitypes.TypedData{}, err
}

types, err := createEIP712Types(messagePayload)
if err != nil {
return apitypes.TypedData{}, err
}

domain := createEIP712Domain(chainID)

typedData := apitypes.TypedData{
Types: types,
PrimaryType: txField,
Domain: domain,
Message: message,
}

return typedData, nil
}
192 changes: 192 additions & 0 deletions ethereum/eip712/eip712_fuzzer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package eip712_test

import (
"fmt"
"strings"

rand "github.com/cometbft/cometbft/libs/rand"
"github.com/evmos/os/ethereum/eip712"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)

type EIP712FuzzTestParams struct {
numTestObjects int
maxNumFieldsPerObject int
minStringLength int
maxStringLength int
randomFloatRange float64
maxArrayLength int
maxObjectDepth int
}

const (
numPrimitiveJSONTypes = 3
numJSONTypes = 5
asciiRangeStart = 65
asciiRangeEnd = 127
fuzzTestName = "Flatten"
)

const (
jsonBoolType = iota
jsonStringType = iota
jsonFloatType = iota
jsonArrayType = iota
jsonObjectType = iota
)

var params = EIP712FuzzTestParams{
numTestObjects: 16,
maxNumFieldsPerObject: 16,
minStringLength: 16,
maxStringLength: 48,
randomFloatRange: 120000000,
maxArrayLength: 8,
maxObjectDepth: 4,
}

// TestRandomPayloadFlattening generates many random payloads with different JSON values to ensure
// that Flattening works across all inputs.
// Note that this is a fuzz test, although it doesn't use Go's Fuzz testing suite, since there are
// variable input sizes, types, and fields. While it may be possible to translate a single input into
// a JSON object, it would require difficult parsing, and ultimately approximates our randomized unit
// tests as they are.
func (suite *EIP712TestSuite) TestRandomPayloadFlattening() {
// Re-seed rand generator
rand.Seed(rand.Int64())

for i := 0; i < params.numTestObjects; i++ {
suite.Run(fmt.Sprintf("%v%d", fuzzTestName, i), func() {
payload := suite.generateRandomPayload(i)

flattened, numMessages, err := eip712.FlattenPayloadMessages(payload)

suite.Require().NoError(err)
suite.Require().Equal(numMessages, i)

suite.verifyPayloadAgainstFlattened(payload, flattened)
})
}
}

// generateRandomPayload creates a random payload of the desired format, with random sub-objects.
func (suite *EIP712TestSuite) generateRandomPayload(numMessages int) gjson.Result {
payload := suite.createRandomJSONObject().Raw
msgs := make([]gjson.Result, numMessages)

for i := 0; i < numMessages; i++ {
msgs[i] = suite.createRandomJSONObject()
}

payload, err := sjson.Set(payload, msgsFieldName, msgs)
suite.Require().NoError(err)

return gjson.Parse(payload)
}

// createRandomJSONObject creates a JSON object with random fields.
func (suite *EIP712TestSuite) createRandomJSONObject() gjson.Result {
var err error
payloadRaw := ""

numFields := suite.createRandomIntInRange(0, params.maxNumFieldsPerObject)
for i := 0; i < numFields; i++ {
key := suite.createRandomString()

randField := suite.createRandomJSONField(i, 0)
payloadRaw, err = sjson.Set(payloadRaw, key, randField)
suite.Require().NoError(err)
}

return gjson.Parse(payloadRaw)
}

// createRandomJSONField creates a random field with a random JSON type, with the possibility of
// nested fields up to depth objects.
func (suite *EIP712TestSuite) createRandomJSONField(t int, depth int) interface{} {
switch t % numJSONTypes {
case jsonBoolType:
return suite.createRandomBoolean()
case jsonStringType:
return suite.createRandomString()
case jsonFloatType:
return suite.createRandomFloat()
case jsonArrayType:
return suite.createRandomJSONNestedArray(depth)
case jsonObjectType:
return suite.createRandomJSONNestedObject(depth)
default:
return nil
}
}

// createRandomJSONNestedArray creates an array of random nested JSON fields.
func (suite *EIP712TestSuite) createRandomJSONNestedArray(depth int) []interface{} {
arr := make([]interface{}, rand.Intn(params.maxArrayLength))
for i := range arr {
arr[i] = suite.createRandomJSONNestedField(depth)
}

return arr
}

// createRandomJSONNestedObject creates a key-value set of objects with random nested JSON fields.
func (suite *EIP712TestSuite) createRandomJSONNestedObject(depth int) interface{} {
numFields := rand.Intn(params.maxNumFieldsPerObject)
obj := make(map[string]interface{})

for i := 0; i < numFields; i++ {
subField := suite.createRandomJSONNestedField(depth)

obj[suite.createRandomString()] = subField
}

return obj
}

// createRandomJSONNestedField serves as a helper for createRandomJSONField and returns a random
// subfield to populate an array or object type.
func (suite *EIP712TestSuite) createRandomJSONNestedField(depth int) interface{} {
var newFieldType int

if depth == params.maxObjectDepth {
newFieldType = rand.Intn(numPrimitiveJSONTypes)
} else {
newFieldType = rand.Intn(numJSONTypes)
}

return suite.createRandomJSONField(newFieldType, depth+1)
}

func (suite *EIP712TestSuite) createRandomBoolean() bool {
return rand.Intn(2) == 0
}

func (suite *EIP712TestSuite) createRandomFloat() float64 {
return (rand.Float64() - 0.5) * params.randomFloatRange
}

func (suite *EIP712TestSuite) createRandomString() string {
bzLen := suite.createRandomIntInRange(params.minStringLength, params.maxStringLength)
bz := make([]byte, bzLen)

for i := 0; i < bzLen; i++ {
bz[i] = byte(suite.createRandomIntInRange(asciiRangeStart, asciiRangeEnd))
}

str := string(bz)

// Remove control characters, since they will make JSON invalid
str = strings.ReplaceAll(str, "{", "")
str = strings.ReplaceAll(str, "}", "")
str = strings.ReplaceAll(str, "]", "")
str = strings.ReplaceAll(str, "[", "")

return str
}

// createRandomIntInRange provides a random integer between [min, max)
func (suite *EIP712TestSuite) createRandomIntInRange(min int, max int) int {
return rand.Intn(max-min) + min
}
Loading

0 comments on commit 48ae68c

Please sign in to comment.