Skip to content

Commit

Permalink
refactor(evm)!: replace HexAddr with EIP55Addr (#2004)
Browse files Browse the repository at this point in the history
* add HexAddrSuite

* remove ToBytes()

* rename ToAddr() to Addr()

* feat: add EIP55Addr

* Update CHANGELOG.md
  • Loading branch information
k-yang authored Aug 16, 2024
1 parent ccb4763 commit ba151df
Show file tree
Hide file tree
Showing 25 changed files with 545 additions and 670 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [#2000](https://github.com/NibiruChain/nibiru/pull/2000) - refactor(evm): simplify ERC-20 keeper methods
- [#2001](https://github.com/NibiruChain/nibiru/pull/2001) - refactor(evm): simplify FunToken methods and tests
- [#2003](https://github.com/NibiruChain/nibiru/pull/2003) - fix(evm): fix FunToken conversions between Cosmos and EVM
- [#2004](https://github.com/NibiruChain/nibiru/pull/2004) - refactor(evm)!: replace `HexAddr` with `EIP55Addr`

#### Dapp modules: perp, spot, oracle, etc

Expand Down
88 changes: 88 additions & 0 deletions eth/eip55.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package eth

import (
"encoding/json"
"fmt"

sdk "github.com/cosmos/cosmos-sdk/types"
gethcommon "github.com/ethereum/go-ethereum/common"
)

var _ sdk.CustomProtobufType = (*EIP55Addr)(nil)

// EIP55Addr is a wrapper around gethcommon.Address that provides JSON marshaling
// and unmarshalling as well as Protobuf serialization and deserialization.
// The constructors ensure that the input string is a valid 20 byte hex address.
type EIP55Addr struct {
gethcommon.Address
}

// Checks input length, but not case-sensitive hex.
func NewEIP55AddrFromStr(input string) (EIP55Addr, error) {
if !gethcommon.IsHexAddress(input) {
return EIP55Addr{}, fmt.Errorf(
"EIP55AddrError: input \"%s\" is not an ERC55-compliant, 20 byte hex address",
input,
)
}

addr := EIP55Addr{
Address: gethcommon.HexToAddress(input),
}

return addr, nil
}

// Marshal implements the gogo proto custom type interface.
// Ref: https://github.com/cosmos/gogoproto/blob/v1.5.0/custom_types.md
func (h EIP55Addr) Marshal() ([]byte, error) {
return h.Bytes(), nil
}

// MarshalJSON returns the [EIP55Addr] as JSON bytes.
// Implements the gogo proto custom type interface.
// Ref: https://github.com/cosmos/gogoproto/blob/v1.5.0/custom_types.md
func (h EIP55Addr) MarshalJSON() ([]byte, error) {
return json.Marshal(h.String())
}

// MarshalTo serializes a EIP55Addr directly into a pre-allocated byte slice ("data").
// MarshalTo implements the gogo proto custom type interface.
// Implements the gogo proto custom type interface.
// Ref: https://github.com/cosmos/gogoproto/blob/v1.5.0/custom_types.md
func (h *EIP55Addr) MarshalTo(data []byte) (n int, err error) {
copy(data, h.Bytes())
return h.Size(), nil
}

// Unmarshal implements the gogo proto custom type interface.
// Ref: https://github.com/cosmos/gogoproto/blob/v1.5.0/custom_types.md
func (h *EIP55Addr) Unmarshal(data []byte) error {
addr := gethcommon.BytesToAddress(data)
*h = EIP55Addr{Address: addr}
return nil
}

// UnmarshalJSON implements the gogo proto custom type interface.
// Ref: https://github.com/cosmos/gogoproto/blob/v1.5.0/custom_types.md
func (h *EIP55Addr) UnmarshalJSON(bz []byte) error {
text := new(string)
if err := json.Unmarshal(bz, text); err != nil {
return err
}

addr, err := NewEIP55AddrFromStr(*text)
if err != nil {
return err
}

*h = addr

return nil
}

// Size implements the gogo proto custom type interface.
// Ref: https://github.com/cosmos/gogoproto/blob/v1.5.0/custom_types.md
func (h EIP55Addr) Size() int {
return len(h.Bytes())
}
175 changes: 175 additions & 0 deletions eth/eip55_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package eth_test

import (
"strconv"
"testing"

gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/suite"

"github.com/NibiruChain/nibiru/v2/eth"
)

// MustNewEIP55AddrFromStr is the same as [NewEIP55AddrFromStr], except it panics
// when there's an error.
func MustNewEIP55AddrFromStr(input string) eth.EIP55Addr {
addr, err := eth.NewEIP55AddrFromStr(input)
if err != nil {
panic(err)
}
return addr
}

var threeValidAddrs []eth.EIP55Addr = []eth.EIP55Addr{
MustNewEIP55AddrFromStr("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"),
MustNewEIP55AddrFromStr("0xAe967917c465db8578ca9024c205720b1a3651A9"),
MustNewEIP55AddrFromStr("0x1111111111111111111112222222222223333323"),
}

func (s *EIP55AddrSuite) TestEquivalence() {
expectedGethAddr := gethcommon.HexToAddress("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed")
expectedEIP55Addr := MustNewEIP55AddrFromStr("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed")

equivalentAddrs := []string{
"0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed",
"0x5AAEB6053F3E94C9B9A09F33669435E7EF1BEAED",
"5aaeb6053f3e94c9b9a09f33669435e7ef1beaed",
"0X5AAEB6053F3E94C9B9A09F33669435E7EF1BEAED",
}

for _, addr := range equivalentAddrs {
eip55Addr, err := eth.NewEIP55AddrFromStr(addr)
s.Require().NoError(err)

s.Equal(expectedEIP55Addr, eip55Addr)
s.Equal(expectedGethAddr, eip55Addr.Address)
}
}

// TestEIP55Addr_NewEIP55Addr: Test to showcase the flexibility of inputs that can be
// passed to `eth.NewEIP55AddrFromStr` and result in a "valid" `EIP55Addr` that preserves
// bijectivity with `gethcommon.Address` and has a canonical string
// representation.
//
// We only want to store valid `EIP55Addr` strings in state. Hex addresses that
// include or remove the prefix, or change the letters to and from lower and
// upper case will all produce the same `EIP55Addr` when passed to
// `eth.NewEIP55AddrFromStr`.
func (s *EIP55AddrSuite) TestNewEIP55Addr() {
// TestCase: An instance of a "EIP55Addr" that derives to the
// expected Ethereum address and results in the same string representation.
type TestCase struct {
input string
name string
wantErr bool
}

want := "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"

for _, tc := range []TestCase{
{
input: "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
name: "happy: no-op (sanity check to show constructor doesn't break a valid input)",
},
{
input: "0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed",
name: "happy: lower case is valid",
},
{
input: "0x5AAEB6053F3E94C9B9A09F33669435E7EF1BEAED",
name: "happy: upper case is valid",
},
{
input: "5aaeb6053f3e94c9b9a09f33669435e7ef1beaed",
name: "happy: 0x prefix: missing",
},
{
input: "0X5aaeb6053f3e94c9b9a09f33669435e7ef1beaed",
name: "happy: 0X prefix: typo",
},
{
input: "nibi1zaa12312312aacbcbeabea123",
name: "sad: bech32 is not hex",
wantErr: true,
},
} {
tc := tc
s.Run(tc.name, func() {
got, err := eth.NewEIP55AddrFromStr(tc.input)
if tc.wantErr {
s.Require().Error(err)
return
}

// string input should give the canonical EIP55Addr
s.Equal(want, got.String())
s.Equal(gethcommon.HexToAddress(tc.input), got.Address)
})
}
}

func (s *EIP55AddrSuite) TestProtobufEncoding() {
for tcIdx, tc := range []struct {
input eth.EIP55Addr
expectedJson string
wantErr string
}{
{
input: threeValidAddrs[0],
expectedJson: "\"0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed\"",
},
{
input: threeValidAddrs[1],
expectedJson: "\"0xAe967917c465db8578ca9024c205720b1a3651A9\"",
},
{
input: threeValidAddrs[2],
expectedJson: "\"0x1111111111111111111112222222222223333323\"",
},
} {
s.Run(strconv.Itoa(tcIdx), func() {
givenMut := tc.input
jsonBz, err := givenMut.MarshalJSON()
s.NoError(err)
s.Equal(tc.expectedJson, string(jsonBz))

eip55Addr := new(eth.EIP55Addr)
s.NoError(eip55Addr.UnmarshalJSON(jsonBz))
s.Equal(givenMut, tc.input,
"Given -> MarshalJSON -> UnmarshalJSON returns a different value than the given when it should be an identity operation (no-op). test case #%d", tcIdx)

bz, err := tc.input.Marshal()
s.NoError(err)
s.Equal(tc.input.Bytes(), bz,
"Marshaling to bytes gives different value than the test case specifies. test case #%d", tcIdx)

err = eip55Addr.Unmarshal(bz)
s.NoError(err)
s.Equal(tc.input.Address, eip55Addr.Address,
"Given -> Marshal -> Unmarshal returns a different value than the given when it should be an identity operation (no-op). test case #%d", tcIdx)

s.Equal(len(tc.input.Bytes()), tc.input.Size())
})
}
}

// showcases how geth checks for valid hex addresses and treats invalid inputs
func (s *EIP55AddrSuite) TestIsEIP55Address() {
s.True(gethcommon.IsHexAddress("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"))
s.True(gethcommon.IsHexAddress("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAED"))
s.False(gethcommon.IsHexAddress("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed1234"))
s.False(gethcommon.IsHexAddress("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1B"))

s.Equal("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed", gethcommon.HexToAddress("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed").Hex())
s.Equal("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed", gethcommon.HexToAddress("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAED").Hex())
s.Equal("0xb6053f3e94c9B9a09f33669435e7eF1BEAEd1234", gethcommon.HexToAddress("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed1234").Hex())
s.Equal("0x00005AaEb6053f3e94c9b9A09f33669435e7Ef1b", gethcommon.HexToAddress("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1B").Hex())
}

type EIP55AddrSuite struct {
suite.Suite
}

func TestEIP55AddrSuite(t *testing.T) {
suite.Run(t, new(EIP55AddrSuite))
}
Loading

0 comments on commit ba151df

Please sign in to comment.