Skip to content

Commit

Permalink
feat(eip-712): Add EIP-712 package. (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
MalteHerrmann authored Aug 1, 2024
1 parent cfa87f2 commit 51d63cd
Show file tree
Hide file tree
Showing 18 changed files with 4,678 additions and 6 deletions.
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
2 changes: 1 addition & 1 deletion .github/workflows/semgrep.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
if: (github.actor != 'dependabot[bot]')
steps:
- name: Permission issue fix
run: git config --global --add safe.directory /__w/evmos/os
run: git config --global --add safe.directory /__w/os/os
- uses: actions/checkout@v4
- name: Get Diff
uses: technote-space/[email protected]
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
}

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 51d63cd

Please sign in to comment.