Skip to content

Commit 42345c6

Browse files
authored
Merge pull request #98 from davecrighton/djc/nestedErrors
Represent nested structure for revert errors when solidity catches and rethrows an error
2 parents 67d77dc + 26efec7 commit 42345c6

5 files changed

Lines changed: 1353 additions & 15 deletions

File tree

internal/signermsgs/en_field_descriptions.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ var (
4848
EIP712ResultR = ffm("EIP712Result.r", "The R value of the ECDSA signature as a 32byte hex encoded array")
4949
EIP712ResultS = ffm("EIP712Result.s", "The S value of the ECDSA signature as a 32byte hex encoded array")
5050

51+
RevertErrorErrorEntry = ffm("RevertError.errorEntry", "The matched ABI error entry at this level of the error chain")
52+
RevertErrorPrefix = ffm("RevertError.prefix", "The readable text prefix before the nested inner error, extracted from the outer Error(string) value")
53+
RevertErrorInnerError = ffm("RevertError.innerError", "The recursively decoded inner error, forming the next level of the error chain, or nil if this is the leaf")
54+
5155
TypedDataDomain = ffm("TypedData.domain", "The data to encode into the EIP712Domain as part fo signing the transaction")
5256
TypedDataMessage = ffm("TypedData.message", "The data to encode into primaryType structure, with nested values for any sub-structures")
5357
TypedDataTypes = ffm("TypedData.types", "Array of types to use when encoding, which must include the primaryType and the EIP712Domain (noting the primary type can be EIP712Domain if the message is empty)")

pkg/abi/abi.go

Lines changed: 94 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -315,19 +315,53 @@ func (a ABI) Errors() map[string]*Entry {
315315
return m
316316
}
317317

318+
// FilterType returns a new ABI containing only the entries whose Type matches t,
319+
// preserving their original order. Returns nil if no entries match.
320+
// Use this to narrow an ABI before passing it to SelectorMap or other helpers,
321+
// e.g. a.FilterType(Error) to obtain only error definitions.
322+
func (a ABI) FilterType(t EntryType) ABI {
323+
var out ABI
324+
for _, e := range a {
325+
if e.Type == t {
326+
out = append(out, e)
327+
}
328+
}
329+
return out
330+
}
331+
332+
// SelectorMap builds a map from 4-byte ABI selectors to their corresponding
333+
// entries. When two entries in the ABI produce the same 4-byte selector, the
334+
// first one wins. Entries for which selector generation fails are silently
335+
// skipped.
336+
//
337+
// SelectorMap operates on all entry types — combine with FilterType to restrict
338+
// to a specific type, e.g. a.FilterType(Error).SelectorMap().
339+
func (a ABI) SelectorMap() map[[4]byte]*Entry {
340+
m := make(map[[4]byte]*Entry)
341+
for _, e := range a {
342+
sel := e.FunctionSelectorBytes()
343+
if len(sel) >= 4 {
344+
var key [4]byte
345+
copy(key[:], sel[:4])
346+
if _, exists := m[key]; !exists {
347+
m[key] = e
348+
}
349+
}
350+
}
351+
return m
352+
}
353+
318354
// Returns the components value from the parsed error
319355
func (a ABI) ParseError(revertData []byte) (*Entry, *ComponentValue, bool) {
320356
return a.ParseErrorCtx(context.Background(), revertData)
321357
}
322358

323-
// Returns the components value from the parsed error
359+
// ParseErrorCtx returns the matched Entry and decoded ComponentValue from the
360+
// given revert data. The ABI's error entries are tried first, followed by the
361+
// built-in Error(string) and Panic(uint256).
324362
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 {
363+
for _, source := range []ABI{a.FilterType(Error), defaultErrorEntries} {
364+
for _, e := range source {
331365
if cv, err := e.DecodeCallDataCtx(ctx, revertData); err == nil {
332366
return e, cv, true
333367
}
@@ -336,20 +370,65 @@ func (a ABI) ParseErrorCtx(ctx context.Context, revertData []byte) (*Entry, *Com
336370
return nil, nil, false
337371
}
338372

339-
func (a ABI) ErrorString(revertData []byte) (string, bool) {
340-
return a.ErrorStringCtx(context.Background(), revertData)
373+
// ErrorFormatOption configures the behaviour of ErrorString and ErrorStringCtx.
374+
type ErrorFormatOption struct {
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
381+
}
382+
383+
// ErrorString formats raw EVM revert data as a human-readable string.
384+
// Pass ErrorFormatOption{SearchForWrappedBinaryErrors: true} to decode inner errors
385+
// that are binary-encoded within an Error(string) value.
386+
func (a ABI) ErrorString(revertData []byte, options ...ErrorFormatOption) (string, bool) {
387+
return a.ErrorStringCtx(context.Background(), revertData, options...)
388+
}
389+
390+
// ErrorStringCtx formats raw EVM revert data as a human-readable string.
391+
// The ABI's own error entries are tried first, followed by the built-in
392+
// Error(string) and Panic(uint256).
393+
// Pass ErrorFormatOption{SearchForWrappedBinaryErrors: true} to decode inner errors
394+
// that are binary-encoded within an Error(string) value.
395+
func (a ABI) ErrorStringCtx(ctx context.Context, revertData []byte, options ...ErrorFormatOption) (string, bool) {
396+
searchBinary := false
397+
for _, o := range options {
398+
searchBinary = searchBinary || o.SearchForWrappedBinaryErrors
399+
}
400+
if searchBinary {
401+
r := a.DecodeRevertErrorCtx(ctx, revertData, ErrorFormatOption{SearchForWrappedBinaryErrors: true})
402+
if r == nil {
403+
return "", false
404+
}
405+
s := r.String()
406+
return s, s != ""
407+
}
408+
e, cv, ok := a.ParseErrorCtx(ctx, revertData)
409+
if !ok {
410+
return "", false
411+
}
412+
s := FormatErrorStringCtx(ctx, e, cv)
413+
return s, s != ""
341414
}
342415

343-
func (a ABI) ErrorStringCtx(ctx context.Context, revertData []byte) (strError string, ok bool) {
344-
e, cv, ok := a.ParseErrorCtx(ctx, revertData)
345-
if ok {
346-
strError = FormatErrorStringCtx(ctx, e, cv)
347-
ok = strError != ""
416+
// SanitizeBinaryString returns the input as a text string if it is entirely
417+
// printable ASCII, or hex-encodes the entire input otherwise. This ensures the
418+
// output is always safe for database TEXT columns and human-readable logging.
419+
func SanitizeBinaryString(raw []byte) string {
420+
for _, b := range raw {
421+
if b < 32 || b >= 127 {
422+
return "0x" + hex.EncodeToString(raw)
423+
}
348424
}
349-
return strError, ok
425+
return string(raw)
350426
}
351427

352428
func FormatErrorStringCtx(ctx context.Context, e *Entry, cv *ComponentValue) string {
429+
if e == nil || cv == nil {
430+
return ""
431+
}
353432
var ok bool
354433
var parsed []interface{}
355434
if res, err := NewSerializer().

0 commit comments

Comments
 (0)