Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eip-712): Add EIP-712 package. #13

Merged
merged 8 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .clconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,28 +22,28 @@
"tests"
],
"change_types": {
"API Breaking": "api\\s*breaking",
"Bug Fixes": "bug\\s*fixes",
"Improvements": "improvements",
"API Breaking": "api\\s*breaking",
"State Machine Breaking": "state\\s*machine\\s*breaking"
},
"expected_spellings": {
"ABI": "abi",
"API": "api",
"CI": "ci",
"Cosmos SDK": "cosmos[\\s-]*sdk",
"CLI": "cli",
"Cosmos SDK": "cosmos[\\s-]*sdk",
"EIP-712": "eip[\\s-]*712",
"ERC-20": "erc[\\s-]*20",
"EVM": "evm",
"evmOS": "evmos",
"IBC": "ibc",
"ICS": "ics",
"ICS-20": "ics[\\s-]*20",
"OS": "os",
"PR": "pr",
"RPC": "rpc",
"SDK": "sdk"
"SDK": "sdk",
"evmOS": "evmos"
},
"legacy_version": null,
"target_repo": "https://github.com/evmos/os"
Expand Down
Empty file added .markdownlintignore
Empty file.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ This changelog was created using the `clu` binary

### Improvements

- (eip-712) [#13](https://github.com/evmos/os/pull/13) Add EIP-712 package.
- (ci) [#12](https://github.com/evmos/os/pull/12) Add CI workflows, configurations, Makefile, License, etc.
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -269,9 +269,12 @@ contracts-add:
### Miscellaneous Checks ###
###############################################################################

# TODO: turn into CI action
check-licenses:
@echo "Checking licenses..."
@python3 scripts/license_checker/check_licenses.py .
@curl -sSfL https://raw.githubusercontent.com/evmos/evmos/v19.0.0/scripts/license_checker/check_licenses.py -o check_licenses.py
@python3 check_licenses.py .
@rm check_licenses.py

check-changelog:
@echo "Checking changelog..."
Expand Down
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
}
MalteHerrmann marked this conversation as resolved.
Show resolved Hide resolved

types, err := createEIP712Types(messagePayload)
if err != nil {
return apitypes.TypedData{}, err
}
MalteHerrmann marked this conversation as resolved.
Show resolved Hide resolved

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
Loading