Skip to content

Commit 26efec7

Browse files
committed
Rename option and update description
Signed-off-by: Dave Crighton <dave.crighton@kaleido.io>
1 parent 77652c2 commit 26efec7

4 files changed

Lines changed: 74 additions & 77 deletions

File tree

pkg/abi/abi.go

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -372,32 +372,33 @@ func (a ABI) ParseErrorCtx(ctx context.Context, revertData []byte) (*Entry, *Com
372372

373373
// ErrorFormatOption configures the behaviour of ErrorString and ErrorStringCtx.
374374
type ErrorFormatOption struct {
375-
// Unwrap causes the full inner error chain to be decoded and formatted,
376-
// handling the Solidity catch-and-rethrow pattern where a contract embeds
377-
// a caught revert inside a new Error(string) via string.concat/string(reason).
378-
// Without this option only the outermost error is formatted.
379-
Unwrap bool
375+
// There is a pattern used in some smart contracts, where a string error is used
376+
// to embed the raw bytes of an ABI encoded sub-error.
377+
// While uncommon, this pattern is supported by the library if you set this switch.
378+
// When set, every standard revert `Error(string)` error will be traversed to look for
379+
// eyecatcher (4 byte signatures) of other errors binary encoded within the string.
380+
SearchForWrappedBinaryErrors bool
380381
}
381382

382383
// ErrorString formats raw EVM revert data as a human-readable string.
383-
// Pass ErrorFormatOption{Unwrap: true} to recursively decode inner errors
384-
// produced by Solidity catch-and-rethrow patterns.
384+
// Pass ErrorFormatOption{SearchForWrappedBinaryErrors: true} to decode inner errors
385+
// that are binary-encoded within an Error(string) value.
385386
func (a ABI) ErrorString(revertData []byte, options ...ErrorFormatOption) (string, bool) {
386387
return a.ErrorStringCtx(context.Background(), revertData, options...)
387388
}
388389

389390
// ErrorStringCtx formats raw EVM revert data as a human-readable string.
390391
// The ABI's own error entries are tried first, followed by the built-in
391392
// Error(string) and Panic(uint256).
392-
// Pass ErrorFormatOption{Unwrap: true} to recursively decode inner errors
393-
// produced by Solidity catch-and-rethrow patterns.
393+
// Pass ErrorFormatOption{SearchForWrappedBinaryErrors: true} to decode inner errors
394+
// that are binary-encoded within an Error(string) value.
394395
func (a ABI) ErrorStringCtx(ctx context.Context, revertData []byte, options ...ErrorFormatOption) (string, bool) {
395-
unwrap := false
396+
searchBinary := false
396397
for _, o := range options {
397-
unwrap = unwrap || o.Unwrap
398+
searchBinary = searchBinary || o.SearchForWrappedBinaryErrors
398399
}
399-
if unwrap {
400-
r := a.DecodeRevertErrorCtx(ctx, revertData, ErrorFormatOption{Unwrap: true})
400+
if searchBinary {
401+
r := a.DecodeRevertErrorCtx(ctx, revertData, ErrorFormatOption{SearchForWrappedBinaryErrors: true})
401402
if r == nil {
402403
return "", false
403404
}

pkg/abi/abi_test.go

Lines changed: 30 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,19 +1162,19 @@ func buildErrorStringABI(msgBytes []byte) []byte {
11621162
return data
11631163
}
11641164

1165-
func TestErrorStringUnwrapPlainError(t *testing.T) {
1165+
func TestErrorStringBinaryWrappedPlainError(t *testing.T) {
11661166
revertData := ethtypes.MustNewHexBytes0xPrefix(
11671167
"0x08c379a0" +
11681168
"0000000000000000000000000000000000000000000000000000000000000020" +
11691169
"000000000000000000000000000000000000000000000000000000000000001a" +
11701170
"4e6f7420656e6f7567682045746865722070726f76696465642e000000000000")
11711171

1172-
result, ok := ABI{}.ErrorString(revertData, ErrorFormatOption{Unwrap: true})
1172+
result, ok := ABI{}.ErrorString(revertData, ErrorFormatOption{SearchForWrappedBinaryErrors: true})
11731173
assert.True(t, ok)
11741174
assert.Equal(t, `Error("Not enough Ether provided.")`, result)
11751175
}
11761176

1177-
func TestErrorStringUnwrapSingleNested(t *testing.T) {
1177+
func TestErrorStringBinaryWrappedSingleNested(t *testing.T) {
11781178
revertData := ethtypes.MustNewHexBytes0xPrefix(
11791179
"0x08c379a00000000000000000000000000000000000000000000000000000000000000020" +
11801180
"000000000000000000000000000000000000000000000000000000000000006b" +
@@ -1185,12 +1185,12 @@ func TestErrorStringUnwrapSingleNested(t *testing.T) {
11851185
"696e6e6572206572726f72206d65737361676500000000000000000000000000" +
11861186
"000000000000000000000000000000000000000000")
11871187

1188-
result, ok := ABI{}.ErrorString(revertData, ErrorFormatOption{Unwrap: true})
1188+
result, ok := ABI{}.ErrorString(revertData, ErrorFormatOption{SearchForWrappedBinaryErrors: true})
11891189
assert.True(t, ok)
11901190
assert.Equal(t, `outer: Error("inner error message")`, result)
11911191
}
11921192

1193-
func TestErrorStringUnwrapDoubleNested(t *testing.T) {
1193+
func TestErrorStringBinaryWrappedDoubleNested(t *testing.T) {
11941194
revertData := ethtypes.MustNewHexBytes0xPrefix(
11951195
"0x08c379a0" +
11961196
"0000000000000000000000000000000000000000000000000000000000000020" +
@@ -1205,12 +1205,12 @@ func TestErrorStringUnwrapDoubleNested(t *testing.T) {
12051205
"000000000000000000000000000000000000000000000000000000000000000d" +
12061206
"64656570657374206572726f720000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
12071207

1208-
result, ok := ABI{}.ErrorString(revertData, ErrorFormatOption{Unwrap: true})
1208+
result, ok := ABI{}.ErrorString(revertData, ErrorFormatOption{SearchForWrappedBinaryErrors: true})
12091209
assert.True(t, ok)
12101210
assert.Equal(t, `level1: level2: Error("deepest error")`, result)
12111211
}
12121212

1213-
func TestErrorStringUnwrapNestedCustomError(t *testing.T) {
1213+
func TestErrorStringBinaryWrappedNestedCustomError(t *testing.T) {
12141214
customABI := ABI{
12151215
{Type: Error, Name: "MyCustomError", Inputs: ParameterArray{{Type: "bytes"}}},
12161216
}
@@ -1229,23 +1229,23 @@ func TestErrorStringUnwrapNestedCustomError(t *testing.T) {
12291229

12301230
// Without the custom ABI the inner error can't be decoded — the
12311231
// outer Error(string) is formatted directly (binary content included)
1232-
result, ok := ABI{}.ErrorString(revertData, ErrorFormatOption{Unwrap: true})
1232+
result, ok := ABI{}.ErrorString(revertData, ErrorFormatOption{SearchForWrappedBinaryErrors: true})
12331233
assert.True(t, ok)
12341234
assert.True(t, strings.HasPrefix(result, `Error("[404]01d`))
12351235

12361236
// With the custom ABI the inner error is decoded
1237-
result, ok = customABI.ErrorString(revertData, ErrorFormatOption{Unwrap: true})
1237+
result, ok = customABI.ErrorString(revertData, ErrorFormatOption{SearchForWrappedBinaryErrors: true})
12381238
assert.True(t, ok)
12391239
assert.Equal(t, `[404]01d - caught bytes:MyCustomError("0xdeadbeef")`, result)
12401240
}
12411241

1242-
func TestErrorStringUnwrapUnknownSelector(t *testing.T) {
1242+
func TestErrorStringBinaryWrappedUnknownSelector(t *testing.T) {
12431243
// Unknown top-level selector
1244-
_, ok := ABI{}.ErrorString([]byte{0x11, 0x22, 0x33, 0x44}, ErrorFormatOption{Unwrap: true})
1244+
_, ok := ABI{}.ErrorString([]byte{0x11, 0x22, 0x33, 0x44}, ErrorFormatOption{SearchForWrappedBinaryErrors: true})
12451245
assert.False(t, ok)
12461246
}
12471247

1248-
func TestErrorStringUnwrapMalformedInnerABI(t *testing.T) {
1248+
func TestErrorStringBinaryWrappedMalformedInner(t *testing.T) {
12491249
defaultErr := &Entry{Type: Error, Name: "Error", Inputs: ParameterArray{{Name: "reason", Type: "string"}}}
12501250
sel := defaultErr.FunctionSelectorBytes()
12511251

@@ -1254,27 +1254,27 @@ func TestErrorStringUnwrapMalformedInnerABI(t *testing.T) {
12541254

12551255
// Malformed inner data can't be decoded, so the outer Error(string)
12561256
// is formatted directly with the raw string content
1257-
result, ok := ABI{}.ErrorString(outerABI, ErrorFormatOption{Unwrap: true})
1257+
result, ok := ABI{}.ErrorString(outerABI, ErrorFormatOption{SearchForWrappedBinaryErrors: true})
12581258
assert.True(t, ok)
12591259
assert.True(t, strings.HasPrefix(result, "Error("))
12601260
}
12611261

1262-
func TestErrorStringUnwrapDepthLimit(t *testing.T) {
1262+
func TestErrorStringBinaryWrappedDepthLimit(t *testing.T) {
12631263
// Build a chain deeper than maxRevertErrorDepth (10)
12641264
data := []byte("leaf")
12651265
for i := 0; i < maxRevertErrorDepth+2; i++ {
12661266
data = buildErrorStringABI(append([]byte("L:"), data...))
12671267
}
12681268

1269-
result, ok := ABI{}.ErrorString(data, ErrorFormatOption{Unwrap: true})
1269+
result, ok := ABI{}.ErrorString(data, ErrorFormatOption{SearchForWrappedBinaryErrors: true})
12701270
assert.True(t, ok)
12711271

12721272
// The chain should be capped — the leaf should not be fully unwrapped
12731273
// through all levels, so the result won't contain a cleanly decoded "leaf"
12741274
assert.NotEmpty(t, result)
12751275
}
12761276

1277-
func TestErrorStringUnwrapCustomBeforeDefault(t *testing.T) {
1277+
func TestErrorStringBinaryWrappedCustomBeforeDefault(t *testing.T) {
12781278
customABI := ABI{
12791279
{Type: Error, Name: "EarlyErr", Inputs: ParameterArray{{Type: "uint256"}}},
12801280
}
@@ -1286,24 +1286,22 @@ func TestErrorStringUnwrapCustomBeforeDefault(t *testing.T) {
12861286
s := "head:" + string(customEncoded) + "middle:" + string(innerErrorABI)
12871287
outerABI := buildErrorStringABI([]byte(s))
12881288

1289-
result, ok := customABI.ErrorString(outerABI, ErrorFormatOption{Unwrap: true})
1289+
result, ok := customABI.ErrorString(outerABI, ErrorFormatOption{SearchForWrappedBinaryErrors: true})
12901290
assert.True(t, ok)
12911291
assert.Equal(t, `head:EarlyErr("42")`, result)
12921292
}
12931293

12941294
// TestErrorStringAssemblyBubbleUp demonstrates that the assembly bubble-up
1295-
// pattern produces legible output from ErrorString — with or without Unwrap.
1295+
// pattern produces legible output from ErrorString — with or without SearchForWrappedBinaryErrors.
12961296
//
1297-
// Solidity's catch-and-rethrow via string.concat wraps the inner error bytes
1298-
// inside an Error(string), so ErrorString without Unwrap returns unreadable
1299-
// binary. The assembly bubble-up pattern:
1297+
// The assembly bubble-up pattern:
13001298
//
13011299
// (bool success, bytes memory result) = target.call(data);
13021300
// if (!success) { assembly { revert(add(32, result), mload(result)) } }
13031301
//
13041302
// passes the raw error bytes through unchanged (no outer wrapper), so the
1305-
// error is decoded directly and is legible either way. Unwrap is not required
1306-
// but is harmless.
1303+
// error is decoded directly and is legible either way. SearchForWrappedBinaryErrors
1304+
// is not required but is harmless.
13071305
// TestAssemblyBubbleUpRealPayloads uses revert bytes captured from a live
13081306
// Solidity deployment on Kaleido (contract AssemblyRevertTest) where each
13091307
// bubbleXxx() function catches its inner revert via `catch (bytes memory data)`
@@ -1384,8 +1382,8 @@ func TestAssemblyBubbleUpRealPayloads(t *testing.T) {
13841382
assert.NoError(t, err)
13851383
assert.Equal(t, tt.wantSig, sig)
13861384

1387-
// Unwrap option must not change the result for non-nested payloads
1388-
result, ok := tt.abi.ErrorString(revertData, ErrorFormatOption{Unwrap: true})
1385+
// SearchForWrappedBinaryErrors must not change the result for non-nested payloads
1386+
result, ok := tt.abi.ErrorString(revertData, ErrorFormatOption{SearchForWrappedBinaryErrors: true})
13891387
assert.True(t, ok)
13901388
assert.Equal(t, tt.wantStr, result)
13911389
})
@@ -1431,21 +1429,21 @@ func TestErrorStringAssemblyBubbleUp(t *testing.T) {
14311429
assert.True(t, ok)
14321430
assert.Equal(t, `InsufficientBalance("100","200")`, result)
14331431

1434-
// With Unwrap: same output — no nesting to unwrap, so result is unchanged.
1435-
resultUnwrap, ok := customABI.ErrorString(rawErrorBytes, ErrorFormatOption{Unwrap: true})
1432+
// With SearchForWrappedBinaryErrors: same output — no nesting, so result is unchanged.
1433+
resultUnwrap, ok := customABI.ErrorString(rawErrorBytes, ErrorFormatOption{SearchForWrappedBinaryErrors: true})
14361434
assert.True(t, ok)
14371435
assert.Equal(t, result, resultUnwrap)
14381436

1439-
// Contrast: string.concat wraps the same error inside Error(string).
1440-
// Without Unwrap the string contains raw binary and is not legible.
1437+
// Contrast: wrapping the same error inside Error(string) embeds binary in the string.
1438+
// Without SearchForWrappedBinaryErrors the string contains raw binary and is not legible.
14411439
wrappedBytes := buildErrorStringABI(append([]byte("outer: "), rawErrorBytes...))
14421440
resultWrappedNoUnwrap, ok := customABI.ErrorString(wrappedBytes)
14431441
assert.True(t, ok)
14441442
assert.False(t, strings.HasPrefix(resultWrappedNoUnwrap, "InsufficientBalance"),
1445-
"string.concat wrapping without Unwrap is not legible")
1443+
"binary-wrapped error without SearchForWrappedBinaryErrors is not legible")
14461444

1447-
// With Unwrap the nesting is decoded and the result is legible again.
1448-
resultWrappedUnwrap, ok := customABI.ErrorString(wrappedBytes, ErrorFormatOption{Unwrap: true})
1445+
// With SearchForWrappedBinaryErrors the inner error is decoded and the result is legible.
1446+
resultWrappedUnwrap, ok := customABI.ErrorString(wrappedBytes, ErrorFormatOption{SearchForWrappedBinaryErrors: true})
14491447
assert.True(t, ok)
14501448
assert.Equal(t, `outer: InsufficientBalance("100","200")`, resultWrappedUnwrap)
14511449
}

pkg/abi/reverterror.go

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ type RevertError struct {
5555
}
5656

5757
// DecodeRevertError decodes raw EVM revert data into a RevertError.
58-
// Pass ErrorFormatOption{Unwrap: true} to recursively decode nested errors
59-
// embedded in Error(string) values by the Solidity catch-and-rethrow pattern.
58+
// Pass ErrorFormatOption{SearchForWrappedBinaryErrors: true} to scan for inner errors
59+
// binary-encoded within an Error(string) value.
6060
// Returns nil if the data does not match any known error selector.
6161
func (a ABI) DecodeRevertError(revertData []byte, options ...ErrorFormatOption) *RevertError {
6262
return a.DecodeRevertErrorCtx(context.Background(), revertData, options...)
@@ -65,30 +65,28 @@ func (a ABI) DecodeRevertError(revertData []byte, options ...ErrorFormatOption)
6565
// DecodeRevertErrorCtx decodes raw EVM revert data into a RevertError.
6666
// The ABI's error entries are tried first, followed by the built-in
6767
// Error(string) and Panic(uint256). Returns nil if no selector matches.
68-
// Pass ErrorFormatOption{Unwrap: true} to recursively decode nested errors
69-
// embedded in Error(string) values by the Solidity catch-and-rethrow pattern.
68+
// Pass ErrorFormatOption{SearchForWrappedBinaryErrors: true} to scan for inner errors
69+
// binary-encoded within an Error(string) value.
7070
func (a ABI) DecodeRevertErrorCtx(ctx context.Context, revertData []byte, options ...ErrorFormatOption) *RevertError {
71-
unwrap := false
71+
searchBinary := false
7272
for _, o := range options {
73-
unwrap = unwrap || o.Unwrap
73+
searchBinary = searchBinary || o.SearchForWrappedBinaryErrors
7474
}
7575
abiErrors := a.FilterType(Error)
7676
for _, source := range []ABI{abiErrors, defaultErrorEntries} {
7777
for _, e := range source {
7878
if cv, err := e.DecodeCallDataCtx(ctx, revertData); err == nil {
7979
r := &RevertError{ErrorEntry: e, cv: cv}
80-
// Only Error(string) is unwrapped for inner errors, because the Solidity
81-
// catch-and-rethrow pattern (string.concat + string(reason)) always
82-
// produces Error(string). Custom errors with string/bytes params that
83-
// also embed error data are not yet handled since there is a high likelihood
84-
// that they are not intended to carry error data.
85-
if unwrap && e.Name == "Error" && len(cv.Children) == 1 {
80+
// Only Error(string) is scanned for binary-wrapped inner errors.
81+
// Custom errors with string/bytes params are not scanned since there
82+
// is a high likelihood that they are not intended to carry error data.
83+
if searchBinary && e.Name == "Error" && len(cv.Children) == 1 {
8684
if strVal, ok := cv.Children[0].Value.(string); ok {
8785
// Build a selector map covering both the caller's error entries
8886
// and the built-in defaults, so inner errors of either kind can
8987
// be recognised during recursive unwrapping.
9088
selectors := append(abiErrors, defaultErrorEntries...).SelectorMap()
91-
r.unwrapInnerError(ctx, selectors, strVal, 0)
89+
r.scanForBinaryWrappedError(ctx, selectors, strVal, 0)
9290
}
9391
}
9492
return r
@@ -98,9 +96,9 @@ func (a ABI) DecodeRevertErrorCtx(ctx context.Context, revertData []byte, option
9896
return nil
9997
}
10098

101-
// unwrapInnerError scans a decoded string value for an embedded ABI error selector.
99+
// scanForBinaryWrappedError scans a decoded string value for an embedded ABI error selector.
102100
// If found, it populates r.Prefix and r.InnerError to form the recursive chain.
103-
func (r *RevertError) unwrapInnerError(ctx context.Context, selectors map[[4]byte]*Entry, s string, depth int) {
101+
func (r *RevertError) scanForBinaryWrappedError(ctx context.Context, selectors map[[4]byte]*Entry, s string, depth int) {
104102
if depth >= maxRevertErrorDepth {
105103
return
106104
}
@@ -121,10 +119,10 @@ func (r *RevertError) unwrapInnerError(ctx context.Context, selectors map[[4]byt
121119
r.Prefix = SanitizeBinaryString(raw[:idx])
122120
r.InnerError = inner
123121

124-
// If the inner error is also Error(string), keep unwrapping
122+
// If the inner error is also Error(string), keep scanning recursively
125123
if entry.Name == "Error" && len(cv.Children) == 1 {
126124
if strVal, ok := cv.Children[0].Value.(string); ok {
127-
inner.unwrapInnerError(ctx, selectors, strVal, depth+1)
125+
inner.scanForBinaryWrappedError(ctx, selectors, strVal, depth+1)
128126
}
129127
}
130128
}
@@ -174,8 +172,8 @@ func (r *RevertError) String() string {
174172

175173
// ErrorString returns the formatted error at this level only, e.g.
176174
// Error("not enough funds") or MyCustomError("0x1234","-100").
177-
// Unlike String(), it does not walk the Cause chain — use it when
178-
// you need the single-level description without recursive unwrapping.
175+
// Unlike String(), it does not walk the chain — use it when
176+
// you need the single-level description.
179177
func (r *RevertError) ErrorString() string {
180178
if r == nil {
181179
return ""
@@ -214,7 +212,7 @@ func (r *RevertError) GetInnerError() *RevertError {
214212
}
215213

216214
// Innermost walks the chain to return the deepest RevertError — the
217-
// original error that triggered the chain of catch-and-rethrow wrappers.
215+
// innermost binary-wrapped error.
218216
func (r *RevertError) Innermost() *RevertError {
219217
if r == nil {
220218
return nil

0 commit comments

Comments
 (0)