@@ -19,6 +19,7 @@ package ethereum
1919import (
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+
198266func 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