Skip to content

Commit ef98682

Browse files
feat(eth): enhance EIP55Addr Unmarshal to support ASCII hex strings (#2371)
* feat(eth): enhance EIP55Addr Unmarshal to support ASCII hex strings Added functionality to the Unmarshal method of EIP55Addr to accept ASCII hex strings (with and without 0x prefix) in addition to raw 20-byte addresses. This improves robustness for clients that may pass hex strings directly. Added corresponding unit tests to validate the new behavior. * chore: changelog * fix: lint * refactor: linter and fmt --------- Co-authored-by: Unique Divine <[email protected]>
1 parent 3da12dc commit ef98682

File tree

3 files changed

+64
-0
lines changed

3 files changed

+64
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ See https://github.com/dangoslen/changelog-enforcer.
4747
-->
4848

4949
- [#2353](https://github.com/NibiruChain/nibiru/pull/2353) - refactor(oracle): remove dead code from asset registry
50+
- [#2371](https://github.com/NibiruChain/nibiru/pull/2371) - feat(evm): fix UnmarshalJSON to accept ASCII hex strings
5051

5152
### Dependencies
5253
- Bump `base-x` from 3.0.10 to 3.0.11 ([#2355](https://github.com/NibiruChain/nibiru/pull/2355))

eth/eip55.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package eth
33
import (
44
"encoding/json"
55
"fmt"
6+
"strings"
67

78
sdk "github.com/cosmos/cosmos-sdk/types"
89
gethcommon "github.com/ethereum/go-ethereum/common"
@@ -58,6 +59,31 @@ func (h *EIP55Addr) MarshalTo(data []byte) (n int, err error) {
5859
// Unmarshal implements the gogo proto custom type interface.
5960
// Ref: https://github.com/cosmos/gogoproto/blob/v1.5.0/custom_types.md
6061
func (h *EIP55Addr) Unmarshal(data []byte) error {
62+
// Fast path: raw 20-byte address (H160)
63+
if len(data) == gethcommon.AddressLength {
64+
addr := gethcommon.BytesToAddress(data)
65+
*h = EIP55Addr{Address: addr}
66+
return nil
67+
}
68+
69+
// If the wire payload looks like an ASCII hex string (with or without 0x),
70+
// parse it as a hex-encoded Ethereum address for robustness across clients
71+
// that accidentally pass a string's UTF-8 bytes (e.g., CosmWasm/Rust SDKs).
72+
s := string(data)
73+
sTrim := strings.TrimSpace(s)
74+
// Normalize 0x prefix handling and length
75+
sNo0x := strings.TrimPrefix(strings.TrimPrefix(sTrim, "0x"), "0X")
76+
if len(sNo0x) == 40 {
77+
// Accept if it is a valid hex address form. geth's IsHexAddress accepts
78+
// both with and without 0x; use the original (possibly 0x-prefixed) string.
79+
if gethcommon.IsHexAddress(sTrim) || gethcommon.IsHexAddress("0x"+sNo0x) {
80+
addr := gethcommon.HexToAddress(sTrim)
81+
*h = EIP55Addr{Address: addr}
82+
return nil
83+
}
84+
}
85+
86+
// Fallback: interpret the bytes directly (uses last 20 bytes if longer).
6187
addr := gethcommon.BytesToAddress(data)
6288
*h = EIP55Addr{Address: addr}
6389
return nil

eth/eip55_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,3 +231,40 @@ func (s *EIP55AddrSuite) TestStringEncoding() {
231231
s.Require().NoError(err)
232232
s.Require().EqualValues(addrHex, newAddr.Hex())
233233
}
234+
235+
// Ensure Unmarshal accepts ASCII hex strings (with/without 0x) in addition to raw 20 bytes.
236+
func (s *EIP55AddrSuite) TestUnmarshalAcceptsASCIIHexStrings() {
237+
expected := gethcommon.HexToAddress("0xb45ad9d1cf36cb1a55cf5f781a791415846465d3")
238+
239+
for idx, input := range []string{
240+
"0xb45ad9d1cf36cb1a55cf5f781a791415846465d3", // lower, with 0x
241+
"b45ad9d1cf36cb1a55cf5f781a791415846465d3", // lower, without 0x
242+
"0xB45AD9D1CF36CB1A55CF5F781A791415846465D3", // upper, with 0x
243+
} {
244+
s.Run("ascii-hex-"+strconv.Itoa(idx), func() {
245+
var got eth.EIP55Addr
246+
// Pass the ASCII hex string bytes to Unmarshal, simulating a client
247+
// that encoded the hex string directly in protobuf bytes.
248+
err := got.Unmarshal([]byte(input))
249+
s.Require().NoError(err)
250+
251+
s.Equal(expected, got.Address)
252+
253+
// Round-trip back to bytes should equal the raw 20 bytes
254+
bz, err := got.Marshal()
255+
s.Require().NoError(err)
256+
s.Equal(expected.Bytes(), bz)
257+
258+
// Size should be exactly 20 bytes
259+
s.Equal(20, got.Size())
260+
})
261+
}
262+
263+
// Raw 20-byte input still works (baseline behavior)
264+
s.Run("raw-20-bytes", func() {
265+
var got eth.EIP55Addr
266+
err := got.Unmarshal(expected.Bytes())
267+
s.Require().NoError(err)
268+
s.Equal(expected, got.Address)
269+
})
270+
}

0 commit comments

Comments
 (0)