Skip to content

Commit 14c6206

Browse files
committed
Best effort to decode nester error strings from reverts
Signed-off-by: Dave Crighton <[email protected]>
1 parent 7c019d2 commit 14c6206

File tree

2 files changed

+434
-1
lines changed

2 files changed

+434
-1
lines changed

internal/ethereum/exec_query.go

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package ethereum
1919
import (
2020
"bytes"
2121
"context"
22+
"encoding/hex"
2223
"encoding/json"
2324
"fmt"
2425

@@ -169,7 +170,7 @@ func processRevertReason(ctx context.Context, outputData ethtypes.HexBytes0xPref
169170
errorInfo, err := defaultError.DecodeCallDataCtx(ctx, outputData)
170171
if err == nil && len(errorInfo.Children) == 1 {
171172
if strError, ok := errorInfo.Children[0].Value.(string); ok {
172-
return strError
173+
return unwrapNestedRevertReasons(ctx, strError, 0, errorAbis)
173174
}
174175
}
175176
log.L(ctx).Warnf("Invalid revert data: %s", outputData)
@@ -195,6 +196,73 @@ func processRevertReason(ctx context.Context, outputData ethtypes.HexBytes0xPref
195196
return ""
196197
}
197198

199+
const maxNestedRevertDepth = 10
200+
201+
// unwrapNestedRevertReasons handles Solidity contracts that catch a revert's raw bytes
202+
// and re-throw them inside a new Error(string) by doing string(reason). This produces
203+
// an Error(string) whose decoded "string" contains raw ABI-encoded error data
204+
// (including null bytes from ABI padding). We scan for all known error selectors
205+
// (Error(string) plus any custom errors from the ABI), decode the earliest match,
206+
// and recurse for nested Error(string) chains.
207+
func unwrapNestedRevertReasons(ctx context.Context, s string, depth int, errorAbis []*abi.Entry) string {
208+
if depth >= maxNestedRevertDepth {
209+
return sanitizeBinaryString([]byte(s))
210+
}
211+
212+
raw := []byte(s)
213+
214+
// Find the earliest occurrence of any known error selector
215+
bestIdx := -1
216+
var bestEntry *abi.Entry
217+
if idx := bytes.Index(raw, defaultErrorID); idx >= 0 {
218+
bestIdx = idx
219+
bestEntry = defaultError
220+
}
221+
for _, e := range errorAbis {
222+
sel := e.FunctionSelectorBytes()
223+
if idx := bytes.Index(raw, sel); idx >= 0 && (bestIdx < 0 || idx < bestIdx) {
224+
bestIdx = idx
225+
bestEntry = e
226+
}
227+
}
228+
229+
if bestIdx < 0 {
230+
return sanitizeBinaryString(raw)
231+
}
232+
233+
prefix := sanitizeBinaryString(raw[:bestIdx])
234+
embedded := raw[bestIdx:]
235+
236+
if bestEntry == defaultError {
237+
errorInfo, err := defaultError.DecodeCallDataCtx(ctx, embedded)
238+
if err == nil && len(errorInfo.Children) == 1 {
239+
if nested, ok := errorInfo.Children[0].Value.(string); ok {
240+
return prefix + unwrapNestedRevertReasons(ctx, nested, depth+1, errorAbis)
241+
}
242+
}
243+
} else {
244+
formatted := formatCustomError(ctx, bestEntry, embedded)
245+
if formatted != "" {
246+
return prefix + formatted
247+
}
248+
}
249+
250+
log.L(ctx).Debugf("Could not decode nested revert at depth %d, hex-encoding remaining %d bytes", depth, len(embedded))
251+
return prefix + "0x" + hex.EncodeToString(embedded)
252+
}
253+
254+
// sanitizeBinaryString returns the input as a text string if it is entirely
255+
// printable ASCII, or hex-encodes the entire input otherwise. This all-or-nothing
256+
// approach avoids guessing where "readable" ends in an ambiguous binary blob.
257+
func sanitizeBinaryString(raw []byte) string {
258+
for _, b := range raw {
259+
if b < 32 || b >= 127 {
260+
return "0x" + hex.EncodeToString(raw)
261+
}
262+
}
263+
return string(raw)
264+
}
265+
198266
func formatCustomError(ctx context.Context, e *abi.Entry, outputData ethtypes.HexBytes0xPrefix) string {
199267
errorInfo, err := e.DecodeCallDataCtx(ctx, outputData)
200268
if err == nil {

0 commit comments

Comments
 (0)