Skip to content

Commit 854d2a7

Browse files
committed
Move unwrap error to use new method
Signed-off-by: Dave Crighton <dave.crighton@kaleido.io>
1 parent e08aac1 commit 854d2a7

2 files changed

Lines changed: 31 additions & 113 deletions

File tree

pkg/abi/abi.go

Lines changed: 12 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -320,14 +320,12 @@ func (a ABI) ParseError(revertData []byte) (*Entry, *ComponentValue, bool) {
320320
return a.ParseErrorCtx(context.Background(), revertData)
321321
}
322322

323-
// Returns the components value from the parsed error
323+
// ParseErrorCtx returns the matched Entry and decoded ComponentValue from the
324+
// given revert data. The ABI's error entries are tried first, followed by the
325+
// built-in Error(string) and Panic(uint256).
324326
func (a ABI) ParseErrorCtx(ctx context.Context, revertData []byte) (*Entry, *ComponentValue, bool) {
325-
// Always include the default error
326-
a = append(ABI{
327-
{Type: Error, Name: "Error", Inputs: ParameterArray{{Name: "reason", Type: "string"}}},
328-
}, a...)
329-
for _, e := range a {
330-
if e.Type == Error {
327+
for _, source := range []ABI{a.errors(), defaultErrorEntries} {
328+
for _, e := range source {
331329
if cv, err := e.DecodeCallDataCtx(ctx, revertData); err == nil {
332330
return e, cv, true
333331
}
@@ -349,103 +347,24 @@ func (a ABI) ErrorStringCtx(ctx context.Context, revertData []byte) (strError st
349347
return strError, ok
350348
}
351349

352-
const maxNestedRevertDepth = 10
353-
354350
// UnwrapErrorStringCtx is like ErrorStringCtx but handles nested errors caused by
355-
// Solidity contracts that catch a revert and re-throw using string(reason). This
356-
// embeds raw ABI-encoded error data (including null bytes from ABI padding) inside
357-
// the new Error(string). The function scans for all known error selectors,
358-
// recursively decodes nested Error(string) chains, and formats known custom errors.
359-
// Any undecoded binary data is hex-encoded.
351+
// Solidity contracts that catch a revert and re-throw using string(reason).
352+
// Delegates to DecodeRevertErrorCtx for structured decoding, then formats the
353+
// result as a human-readable string.
360354
func (a ABI) UnwrapErrorStringCtx(ctx context.Context, revertData []byte) (string, bool) {
361-
e, cv, ok := a.ParseErrorCtx(ctx, revertData)
362-
if !ok {
355+
r := a.DecodeRevertErrorCtx(ctx, revertData)
356+
if r == nil {
363357
return "", false
364358
}
365-
// For Error(string), unwrap any nested errors inside the decoded string
366-
if e.Name == "Error" && len(cv.Children) == 1 {
367-
if strVal, ok := cv.Children[0].Value.(string); ok {
368-
return unwrapNestedRevertReasons(ctx, a, strVal, 0), true
369-
}
370-
}
371-
// For other error types, format directly
372-
strError := FormatErrorStringCtx(ctx, e, cv)
373-
return strError, strError != ""
359+
s := r.String()
360+
return s, s != ""
374361
}
375362

376363
// UnwrapErrorString is a convenience wrapper for UnwrapErrorStringCtx.
377364
func (a ABI) UnwrapErrorString(revertData []byte) (string, bool) {
378365
return a.UnwrapErrorStringCtx(context.Background(), revertData)
379366
}
380367

381-
// unwrapNestedRevertReasons recursively decodes Error(string) values that contain
382-
// embedded ABI-encoded error data from Solidity catch-and-rethrow patterns.
383-
func unwrapNestedRevertReasons(ctx context.Context, a ABI, s string, depth int) string {
384-
if depth >= maxNestedRevertDepth {
385-
return SanitizeBinaryString([]byte(s))
386-
}
387-
388-
raw := []byte(s)
389-
390-
// Build a lookup map of 4-byte selectors so we can scan the string once
391-
type selectorKey = [4]byte
392-
selectors := make(map[selectorKey]*Entry)
393-
errorsWithDefault := append(ABI{
394-
{Type: Error, Name: "Error", Inputs: ParameterArray{{Name: "reason", Type: "string"}}},
395-
}, a...)
396-
for _, e := range errorsWithDefault {
397-
if e.Type != Error {
398-
continue
399-
}
400-
sel := e.FunctionSelectorBytes()
401-
if len(sel) >= 4 {
402-
var key selectorKey
403-
copy(key[:], sel[:4])
404-
if _, exists := selectors[key]; !exists {
405-
selectors[key] = e
406-
}
407-
}
408-
}
409-
410-
// Single pass: walk through the bytes looking for any known selector
411-
bestIdx := -1
412-
var bestEntry *Entry
413-
if len(raw) >= 4 {
414-
for i := 0; i <= len(raw)-4; i++ {
415-
var key selectorKey
416-
copy(key[:], raw[i:i+4])
417-
if e, ok := selectors[key]; ok {
418-
bestIdx = i
419-
bestEntry = e
420-
break
421-
}
422-
}
423-
}
424-
425-
if bestIdx < 0 {
426-
return SanitizeBinaryString(raw)
427-
}
428-
429-
prefix := SanitizeBinaryString(raw[:bestIdx])
430-
embedded := raw[bestIdx:]
431-
432-
cv, err := bestEntry.DecodeCallDataCtx(ctx, embedded)
433-
if err == nil {
434-
if bestEntry.Name == "Error" && len(cv.Children) == 1 {
435-
if nested, ok := cv.Children[0].Value.(string); ok {
436-
return prefix + unwrapNestedRevertReasons(ctx, a, nested, depth+1)
437-
}
438-
}
439-
formatted := FormatErrorStringCtx(ctx, bestEntry, cv)
440-
if formatted != "" {
441-
return prefix + formatted
442-
}
443-
}
444-
445-
log.L(ctx).Debugf("Could not decode nested revert at depth %d, hex-encoding remaining %d bytes", depth, len(embedded))
446-
return prefix + "0x" + hex.EncodeToString(embedded)
447-
}
448-
449368
// SanitizeBinaryString returns the input as a text string if it is entirely
450369
// printable ASCII, or hex-encodes the entire input otherwise. This ensures the
451370
// output is always safe for database TEXT columns and human-readable logging.

pkg/abi/abi_test.go

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1104,7 +1104,7 @@ func TestUnwrapErrorStringPlainError(t *testing.T) {
11041104

11051105
result, ok := ABI{}.UnwrapErrorString(revertData)
11061106
assert.True(t, ok)
1107-
assert.Equal(t, "Not enough Ether provided.", result)
1107+
assert.Equal(t, `Error("Not enough Ether provided.")`, result)
11081108
}
11091109

11101110
func TestUnwrapErrorStringSingleNested(t *testing.T) {
@@ -1120,7 +1120,7 @@ func TestUnwrapErrorStringSingleNested(t *testing.T) {
11201120

11211121
result, ok := ABI{}.UnwrapErrorString(revertData)
11221122
assert.True(t, ok)
1123-
assert.Equal(t, "outer: inner error message", result)
1123+
assert.Equal(t, `outer: Error("inner error message")`, result)
11241124
}
11251125

11261126
func TestUnwrapErrorStringDoubleNested(t *testing.T) {
@@ -1140,7 +1140,7 @@ func TestUnwrapErrorStringDoubleNested(t *testing.T) {
11401140

11411141
result, ok := ABI{}.UnwrapErrorString(revertData)
11421142
assert.True(t, ok)
1143-
assert.Equal(t, "level1: level2: deepest error", result)
1143+
assert.Equal(t, `level1: level2: Error("deepest error")`, result)
11441144
}
11451145

11461146
func TestUnwrapErrorStringNestedCustomError(t *testing.T) {
@@ -1160,10 +1160,11 @@ func TestUnwrapErrorStringNestedCustomError(t *testing.T) {
11601160
"deadbeef00000000000000000000000000000000000000000000000000000000" +
11611161
"00000000")
11621162

1163-
// Without the custom ABI, the nested section can't be decoded
1163+
// Without the custom ABI, the nested section can't be decoded — the
1164+
// outer Error(string) is formatted directly (binary content included)
11641165
result, ok := ABI{}.UnwrapErrorString(revertData)
11651166
assert.True(t, ok)
1166-
assert.True(t, strings.HasPrefix(result, "0x"))
1167+
assert.True(t, strings.HasPrefix(result, `Error("[404]01d`))
11671168

11681169
// With the custom ABI, the nested error is decoded
11691170
result, ok = customABI.UnwrapErrorString(revertData)
@@ -1184,28 +1185,26 @@ func TestUnwrapErrorStringMalformedNestedABI(t *testing.T) {
11841185
badData := "prefix:" + string(sel) + "truncated"
11851186
outerABI := buildErrorStringABI([]byte(badData))
11861187

1188+
// Malformed nested data can't be decoded, so the outer Error(string)
1189+
// is formatted directly with the raw string content
11871190
result, ok := ABI{}.UnwrapErrorString(outerABI)
11881191
assert.True(t, ok)
1189-
assert.Equal(t, "prefix:0x08c379a07472756e6361746564", result)
1192+
assert.True(t, strings.HasPrefix(result, "Error("))
11901193
}
11911194

11921195
func TestUnwrapErrorStringDepthLimit(t *testing.T) {
1193-
innerABI := buildErrorStringABI([]byte("should not decode"))
1194-
s := "prefix:" + string(innerABI)
1195-
outerABI := buildErrorStringABI([]byte(s))
1196-
1197-
// Passes through processRevertReason, then into unwrap at depth 0.
1198-
// We can't easily test depth=10 through the public API without 10 levels of nesting,
1199-
// so test the internal function directly.
1200-
result := unwrapNestedRevertReasons(nil, ABI{}, s, maxNestedRevertDepth)
1201-
assert.True(t, strings.HasPrefix(result, "0x"))
1202-
assert.NotEqual(t, "prefix:should not decode", result)
1196+
// Build a chain deeper than maxRevertErrorDepth (10)
1197+
data := []byte("leaf")
1198+
for i := 0; i < maxRevertErrorDepth+2; i++ {
1199+
data = buildErrorStringABI(append([]byte("L:"), data...))
1200+
}
12031201

1204-
// At depth max-1, it still decodes
1205-
result = unwrapNestedRevertReasons(nil, ABI{}, s, maxNestedRevertDepth-1)
1206-
assert.Equal(t, "prefix:should not decode", result)
1202+
result, ok := ABI{}.UnwrapErrorString(data)
1203+
assert.True(t, ok)
12071204

1208-
_ = outerABI // suppress unused
1205+
// The chain should be capped — the leaf should not be fully unwrapped
1206+
// through all levels, so the result won't contain a cleanly decoded "leaf"
1207+
assert.NotEmpty(t, result)
12091208
}
12101209

12111210
func TestUnwrapErrorStringCustomBeforeDefaultError(t *testing.T) {

0 commit comments

Comments
 (0)