fix: Rework revert_reason (#9212)

* fix: rework revert_reason

* fix: format

* fix: tests

* fix: chain_type_fields

* chore: refactor

* fix: decode functions refactor
mf-only-health-webapp
Kirill Fedoseev 6 months ago committed by GitHub
parent 4d4f355b0d
commit 7f6ee61c10
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 8
      apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_function_response.html.eex
  2. 2
      apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex
  3. 4
      apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex
  4. 10
      apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex
  5. 28
      apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex
  6. 63
      apps/block_scout_web/test/block_scout_web/controllers/api/rpc/transaction_controller_test.exs
  7. 16
      apps/block_scout_web/test/block_scout_web/views/transaction_view_test.exs
  8. 46
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/nethermind/trace.ex
  9. 6
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/trace_replay_block_transactions.ex
  10. 16
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex
  11. 136
      apps/explorer/lib/explorer/chain.ex
  12. 1
      apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex
  13. 39
      apps/explorer/lib/explorer/chain/internal_transaction.ex
  14. 61
      apps/explorer/lib/explorer/chain/transaction.ex
  15. 54
      apps/explorer/test/explorer/chain_test.exs

@ -4,8 +4,8 @@
</pre>
<%= case @outputs do %>
<% {:error, %{code: code, message: message, data: data}} -> %>
<% revert_reason = Chain.format_revert_reason_message(data) %>
<% {:error, %{code: code, message: message, data: _data} = error} -> %>
<% revert_reason = Chain.parse_revert_reason_from_error(error) %>
<%= case decode_revert_reason(@smart_contract_address, revert_reason) do %>
<% {:ok, _identifier, text, mapping} -> %>
<pre><%= raw(values_with_type(text, :error, nil)) %></pre>
@ -15,7 +15,7 @@
<% end %>
</div>
<% {:error, _contract_verified, []} -> %>
<% decoded_revert_reason = decode_hex_revert_reason(revert_reason) %>
<% decoded_revert_reason = BlockScoutWeb.TransactionView.decode_hex_revert_reason_as_utf8(revert_reason) %>
<pre><span class="text-muted"><div style="padding-left: 20px"><%= "(#{code}) #{message} (#{if String.valid?(decoded_revert_reason), do: decoded_revert_reason, else: revert_reason})" %></div></span></pre>
<% {:error, _contract_verified, candidates} -> %>
<% {:ok, _identifier, text, mapping} = Enum.at(candidates, 0) %>
@ -26,7 +26,7 @@
<% end %>
</div>
<% _ -> %>
<% decoded_revert_reason = decode_hex_revert_reason(revert_reason) %>
<% decoded_revert_reason = BlockScoutWeb.TransactionView.decode_hex_revert_reason_as_utf8(revert_reason) %>
<pre><span class="text-muted"><div style="padding-left: 20px"><%= "(#{code}) #{message} (#{if String.valid?(decoded_revert_reason), do: decoded_revert_reason, else: revert_reason})" %></div></span></pre>
<% end %>
<% {:error, %{code: code, message: message}} -> %>

@ -145,7 +145,7 @@
<%= render(BlockScoutWeb.TransactionView, "_decoded_input_body.html", method_id: method_id, text: text, mapping: mapping, error: true) %>
<% _ -> %>
<% hex = BlockScoutWeb.TransactionView.get_pure_transaction_revert_reason(@transaction) %>
<% utf8 = BlockScoutWeb.TransactionView.decoded_revert_reason(@transaction) %>
<% utf8 = BlockScoutWeb.TransactionView.decode_revert_reason_as_utf8(hex) %>
<div class="tile tile-muted">
<pre class="pre-scrollable pre-scrollable-shorty pre-wrap mb-0"><code>Raw:<%= raw("\t") %><%= hex %><%= raw("\n") %>UTF-8:<%= raw("\t") %><%= utf8 %></code></pre>
</div>

@ -55,8 +55,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do
def prepare_function_response(outputs, names, contract_address_hash) do
case outputs do
{:error, %{code: code, message: message, data: data}} ->
revert_reason = Chain.format_revert_reason_message(data)
{:error, %{code: code, message: message, data: _data} = error} ->
revert_reason = Chain.parse_revert_reason_from_error(error)
case SmartContractView.decode_revert_reason(contract_address_hash, revert_reason, @api_true) do
{:ok, method_id, text, mapping} ->

@ -221,16 +221,6 @@ defmodule BlockScoutWeb.SmartContractView do
)
end
def decode_hex_revert_reason(hex_revert_reason) do
case Integer.parse(hex_revert_reason, 16) do
{number, ""} ->
:binary.encode_unsigned(number)
_ ->
hex_revert_reason
end
end
def not_last_element?(length, index), do: length > 1 and index < length - 1
def cut_rpc_url(error) do

@ -609,28 +609,26 @@ defmodule BlockScoutWeb.TransactionView do
end
# Function decodes revert reason of the transaction
@spec decoded_revert_reason(Transaction.t() | nil) :: binary() | nil
def decoded_revert_reason(transaction) do
revert_reason = get_pure_transaction_revert_reason(transaction)
@spec decode_revert_reason_as_utf8(binary() | nil) :: binary() | nil
def decode_revert_reason_as_utf8(revert_reason) do
case revert_reason do
nil ->
nil
"0x" <> hex_part ->
process_hex_revert_reason(hex_part)
decode_hex_revert_reason_as_utf8(hex_part)
hex_part ->
process_hex_revert_reason(hex_part)
decode_hex_revert_reason_as_utf8(hex_part)
end
end
# Function converts hex revert reason to the binary
@spec process_hex_revert_reason(nil) :: nil
defp process_hex_revert_reason(nil), do: nil
@spec process_hex_revert_reason(binary()) :: binary()
defp process_hex_revert_reason(hex_revert_reason) do
case Integer.parse(hex_revert_reason, 16) do
{number, ""} ->
:binary.encode_unsigned(number)
# Function converts hex revert reason to the utf8 binary
@spec decode_hex_revert_reason_as_utf8(binary()) :: binary()
def decode_hex_revert_reason_as_utf8(hex_revert_reason) do
case Base.decode16(hex_revert_reason, case: :mixed) do
{:ok, revert_reason} ->
revert_reason
_ ->
hex_revert_reason

@ -660,11 +660,25 @@ defmodule BlockScoutWeb.API.RPC.TransactionControllerTest do
insert(:address)
# Error("No credit of that type")
hex_reason =
"0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000164e6f20637265646974206f662074686174207479706500000000000000000000"
# fail to trace_replayTransaction
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn _json, [] ->
{:error, %{code: -32015, message: "VM execution error.", data: "revert: No credit of that type"}}
{:error, :econnrefused}
end
)
# fallback to eth_call
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn _json, [] ->
{:error, %{code: -32015, message: "VM execution error.", data: hex_reason}}
end
)
@ -679,7 +693,7 @@ defmodule BlockScoutWeb.API.RPC.TransactionControllerTest do
|> get("/api", params)
|> json_response(200)
assert response["result"]["revertReason"] == "No credit of that type"
assert response["result"]["revertReason"] == hex_reason
assert response["status"] == "1"
assert response["message"] == "OK"
end
@ -707,7 +721,38 @@ defmodule BlockScoutWeb.API.RPC.TransactionControllerTest do
EthereumJSONRPC.Mox,
:json_rpc,
fn _json, [] ->
{:error, %{code: -32015, message: "VM execution error.", data: ""}}
{:ok,
[
%{
id: 0,
result: %{
"output" => "0x",
"stateDiff" => nil,
"trace" => [
%{
"action" => %{
"callType" => "call",
"from" => "0x6a17ca3bbf83764791f4a9f2b4dbbaebbc8b3e0d",
"gas" => "0x5208",
"input" => "0x01",
"to" => "0x7ed1e469fcb3ee19c0366d829e291451be638e59",
"value" => "0x0"
},
"error" => "Reverted",
"result" => %{
"gasUsed" => "0x5208",
"output" => "0x"
},
"subtraces" => 0,
"traceAddress" => [],
"type" => "call"
}
],
"transactionHash" => "0xac2a7dab94d965893199e7ee01649e2d66f0787a4c558b3118c09e80d4df8269",
"vmTrace" => nil
}
}
]}
end
)
@ -722,7 +767,7 @@ defmodule BlockScoutWeb.API.RPC.TransactionControllerTest do
|> get("/api", params)
|> json_response(200)
assert response["result"]["revertReason"] == ""
assert response["result"]["revertReason"] == "0x"
assert response["status"] == "1"
assert response["message"] == "OK"
end
@ -745,6 +790,16 @@ defmodule BlockScoutWeb.API.RPC.TransactionControllerTest do
insert(:address)
# fail to trace_replayTransaction
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn _json, [] ->
{:error, :econnrefused}
end
)
# fallback to eth_call
expect(
EthereumJSONRPC.Mox,
:json_rpc,

@ -293,6 +293,16 @@ defmodule BlockScoutWeb.TransactionViewTest do
test "handles transactions with gas_price set to nil" do
transaction = insert(:transaction, gas_price: nil, error: "execution reverted")
# fail to trace_replayTransaction
EthereumJSONRPC.Mox
|> expect(
:json_rpc,
fn _json, [] ->
{:error, :econnrefused}
end
)
# fallback to eth_call
EthereumJSONRPC.Mox
|> expect(:json_rpc, fn %{
id: 0,
@ -312,7 +322,11 @@ defmodule BlockScoutWeb.TransactionViewTest do
revert_reason = TransactionView.transaction_revert_reason(transaction, nil)
assert revert_reason == {:error, :not_a_contract_call}
assert revert_reason ==
{:ok, "08c379a0", "Error(string reason)",
[
{"reason", "string", "UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT"}
]}
end
end
end

@ -4,6 +4,7 @@ defmodule EthereumJSONRPC.Nethermind.Trace do
[`trace_replayTransaction`](https://openethereum.github.io/JSONRPC-trace-module#trace_replaytransaction).
"""
import EthereumJSONRPC.Transaction, only: [put_if_present: 3]
alias EthereumJSONRPC.Nethermind.Trace.{Action, Result}
@doc """
@ -232,7 +233,13 @@ defmodule EthereumJSONRPC.Nethermind.Trace do
input: input,
value: value
}
|> put_call_error_or_result(elixir)
|> put_if_present(elixir, [
{"error", :error}
])
|> put_if_present(elixir |> Map.get("result", %{}), [
{"gasUsed", :gas_used},
{"output", :output}
])
end
def elixir_to_params(%{"type" => "create" = type} = elixir) do
@ -257,7 +264,14 @@ defmodule EthereumJSONRPC.Nethermind.Trace do
value: value,
transaction_index: transaction_index
}
|> put_create_error_or_result(elixir)
|> put_if_present(elixir, [
{"error", :error}
])
|> put_if_present(elixir |> Map.get("result", %{}), [
{"gasUsed", :gas_used},
{"code", :created_contract_code},
{"address", :created_contract_address_hash}
])
end
def elixir_to_params(%{"type" => "suicide"} = elixir) do
@ -470,32 +484,4 @@ defmodule EthereumJSONRPC.Nethermind.Trace do
end
defp entry_to_elixir({"transactionIndex", index} = entry) when is_integer(index), do: entry
defp put_call_error_or_result(params, %{
"result" => %{"gasUsed" => gas_used, "output" => output}
}) do
Map.merge(params, %{gas_used: gas_used, output: output})
end
defp put_call_error_or_result(params, %{"error" => error}) do
Map.put(params, :error, error)
end
defp put_create_error_or_result(params, %{
"result" => %{
"address" => created_contract_address_hash,
"code" => code,
"gasUsed" => gas_used
}
}) do
Map.merge(params, %{
created_contract_code: code,
created_contract_address_hash: created_contract_address_hash,
gas_used: gas_used
})
end
defp put_create_error_or_result(params, %{"error" => error}) do
Map.put(params, :error, error)
end
end

@ -1,6 +1,6 @@
defmodule EthereumJSONRPC.TraceReplayBlockTransactions do
@moduledoc """
Methods for processing the data from `trace_replayBlockTransactions` JSON RPC method
Methods for processing the data from `trace_replayTransaction` and `trace_replayBlockTransactions` JSON RPC methods
"""
require Logger
import EthereumJSONRPC, only: [id_to_params: 1, integer_to_quantity: 1, json_rpc: 2, request: 1]
@ -144,8 +144,8 @@ defmodule EthereumJSONRPC.TraceReplayBlockTransactions do
request(%{id: id, method: "trace_replayBlockTransactions", params: [integer_to_quantity(block_number), ["trace"]]})
end
def trace_replay_transaction_responses_to_first_trace_params(responses, id_to_params, traces_module)
when is_list(responses) and is_map(id_to_params) do
defp trace_replay_transaction_responses_to_first_trace_params(responses, id_to_params, traces_module)
when is_list(responses) and is_map(id_to_params) do
with {:ok, traces} <- trace_replay_transaction_responses_to_first_trace(responses, id_to_params) do
params =
traces

@ -290,7 +290,7 @@ defmodule EthereumJSONRPC.Transaction do
max_fee_per_gas: max_fee_per_gas
}
put_if_present(transaction, result, [
put_if_present(result, transaction, [
{"creates", :created_contract_address_hash},
{"block_timestamp", :block_timestamp},
{"r", :r},
@ -336,7 +336,7 @@ defmodule EthereumJSONRPC.Transaction do
max_fee_per_gas: max_fee_per_gas
}
put_if_present(transaction, result, [
put_if_present(result, transaction, [
{"creates", :created_contract_address_hash},
{"block_timestamp", :block_timestamp},
{"r", :r},
@ -378,7 +378,7 @@ defmodule EthereumJSONRPC.Transaction do
type: type
}
put_if_present(transaction, result, [
put_if_present(result, transaction, [
{"creates", :created_contract_address_hash},
{"block_timestamp", :block_timestamp},
{"r", :r},
@ -418,7 +418,7 @@ defmodule EthereumJSONRPC.Transaction do
transaction_index: index
}
put_if_present(transaction, result, [
put_if_present(result, transaction, [
{"creates", :created_contract_address_hash},
{"block_timestamp", :block_timestamp},
{"r", :r},
@ -459,7 +459,7 @@ defmodule EthereumJSONRPC.Transaction do
type: type
}
put_if_present(transaction, result, [
put_if_present(result, transaction, [
{"creates", :created_contract_address_hash},
{"block_timestamp", :block_timestamp},
{"r", :r},
@ -471,14 +471,14 @@ defmodule EthereumJSONRPC.Transaction do
defp chain_type_fields(params, elixir) do
case Application.get_env(:explorer, :chain_type) do
:ethereum ->
put_if_present(elixir, params, [
put_if_present(params, elixir, [
{"blobVersionedHashes", :blob_versioned_hashes},
{"maxFeePerBlobGas", :max_fee_per_blob_gas}
])
:optimism ->
# we need to put blobVersionedHashes for Indexer.Fetcher.Optimism.TxnBatch module
put_if_present(elixir, params, [
put_if_present(params, elixir, [
{"l1TxOrigin", :l1_tx_origin},
{"l1BlockNumber", :l1_block_number},
{"blobVersionedHashes", :blob_versioned_hashes}
@ -670,7 +670,7 @@ defmodule EthereumJSONRPC.Transaction do
{nil, nil}
end
defp put_if_present(transaction, result, keys) do
def put_if_present(result, transaction, keys) do
Enum.reduce(keys, result, fn {from_key, to_key}, acc ->
value = transaction[from_key]

@ -118,8 +118,6 @@ defmodule Explorer.Chain do
@revert_msg_prefix_4 "Reverted "
# Geth-like node
@revert_msg_prefix_5 "execution reverted: "
# keccak256("Error(string)")
@revert_error_method_id "08c379a0"
@limit_showing_transactions 10_000
@default_page_size 50
@ -2956,17 +2954,58 @@ defmodule Explorer.Chain do
end
end
def fetch_tx_revert_reason(
%Transaction{
block_number: block_number,
to_address_hash: to_address_hash,
from_address_hash: from_address_hash,
input: data,
gas: gas,
gas_price: gas_price,
value: value
} = transaction
) do
def fetch_tx_revert_reason(transaction) do
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
hash_string = to_string(transaction.hash)
response =
fetch_first_trace(
[
%{
block_hash: transaction.block_hash,
block_number: transaction.block_number,
hash_data: hash_string,
transaction_index: transaction.index
}
],
json_rpc_named_arguments
)
revert_reason =
case response do
{:ok, first_trace_params} ->
first_trace_params |> Enum.at(0) |> Map.get(:output, %Data{bytes: <<>>}) |> to_string()
{:error, reason} ->
Logger.error(fn ->
["Error while fetching first trace for tx: #{hash_string} error reason: ", reason]
end)
fetch_tx_revert_reason_using_call(transaction)
:ignore ->
fetch_tx_revert_reason_using_call(transaction)
end
if !is_nil(revert_reason) do
transaction
|> Changeset.change(%{revert_reason: revert_reason})
|> Repo.update()
end
revert_reason
end
defp fetch_tx_revert_reason_using_call(%Transaction{
block_number: block_number,
to_address_hash: to_address_hash,
from_address_hash: from_address_hash,
input: data,
gas: gas,
gas_price: gas_price,
value: value
}) do
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
req =
@ -2981,31 +3020,33 @@ defmodule Explorer.Chain do
Wei.hex_format(value)
)
revert_reason =
case EthereumJSONRPC.json_rpc(req, json_rpc_named_arguments) do
{:error, %{data: data}} ->
data
case EthereumJSONRPC.json_rpc(req, json_rpc_named_arguments) do
{:error, error} ->
parse_revert_reason_from_error(error)
{:error, %{message: message}} ->
message
_ ->
nil
end
end
_ ->
""
end
def parse_revert_reason_from_error(%{data: data}), do: format_revert_data(data)
formatted_revert_reason =
revert_reason |> format_revert_reason_message() |> (&if(String.valid?(&1), do: &1, else: revert_reason)).()
def parse_revert_reason_from_error(%{message: message}), do: format_revert_reason_message(message)
if byte_size(formatted_revert_reason) > 0 do
transaction
|> Changeset.change(%{revert_reason: formatted_revert_reason})
|> Repo.update()
end
defp format_revert_data(revert_data) do
case revert_data do
"revert" ->
"0x"
formatted_revert_reason
"0x" <> _ ->
revert_data
_ ->
nil
end
end
def format_revert_reason_message(revert_reason) do
defp format_revert_reason_message(revert_reason) do
case revert_reason do
@revert_msg_prefix_1 <> rest ->
rest
@ -3014,41 +3055,16 @@ defmodule Explorer.Chain do
rest
@revert_msg_prefix_3 <> rest ->
extract_revert_reason_message_wrapper(rest)
rest
@revert_msg_prefix_4 <> rest ->
extract_revert_reason_message_wrapper(rest)
rest
@revert_msg_prefix_5 <> rest ->
extract_revert_reason_message_wrapper(rest)
revert_reason_full ->
revert_reason_full
end
end
defp extract_revert_reason_message_wrapper(revert_reason_message) do
case revert_reason_message do
"0x" <> hex ->
extract_revert_reason_message(hex)
_ ->
revert_reason_message
end
end
defp extract_revert_reason_message(hex) do
case hex do
@revert_error_method_id <> msg_with_offset ->
[msg] =
msg_with_offset
|> Base.decode16!(case: :mixed)
|> TypeDecoder.decode_raw([:string])
msg
rest
_ ->
hex
nil
end
end

@ -661,6 +661,7 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do
updated_at: timestamps.updated_at
]
# we don't save reverted trace outputs, but if we did, we could also set :revert_reason here
set =
default_set
|> put_status_in_update_set(first_trace, transaction_from_db)

@ -232,6 +232,7 @@ defmodule Explorer.Chain.InternalTransaction do
Failed `:call`s are not allowed to set `gas_used` or `output` because they are part of the successful `result` object
in the Nethermind JSONRPC response. They still need `input`, however.
The changeset will be fixed by `validate_call_error_or_result`, therefore the changeset is still valid.
iex> changeset = Explorer.Chain.InternalTransaction.changeset(
...> %Explorer.Chain.InternalTransaction{},
@ -256,11 +257,7 @@ defmodule Explorer.Chain.InternalTransaction do
...> }
...> )
iex> changeset.valid?
false
iex> changeset.errors
[
output: {"can't be present for failed call", []}
]
true
Likewise, successful `:call`s require `input`, `gas_used` and `output` to be set.
@ -293,6 +290,7 @@ defmodule Explorer.Chain.InternalTransaction do
For failed `:create`, `created_contract_code`, `created_contract_address_hash`, and `gas_used` are not allowed to be
set because they come from `result` object, which shouldn't be returned from Nethermind.
The changeset will be fixed by `validate_create_error_or_result`, therefore the changeset is still valid.
iex> changeset = Explorer.Chain.InternalTransaction.changeset(
...> %Explorer.Chain.InternalTransaction{},
@ -316,13 +314,7 @@ defmodule Explorer.Chain.InternalTransaction do
...> }
iex> )
iex> changeset.valid?
false
iex> changeset.errors
[
gas_used: {"can't be present for failed create", []},
created_contract_address_hash: {"can't be present for failed create", []},
created_contract_code: {"can't be present for failed create", []}
]
true
For successful `:create`, `created_contract_code`, `created_contract_address_hash`, and `gas_used` are required.
@ -420,6 +412,7 @@ defmodule Explorer.Chain.InternalTransaction do
changeset
|> cast(attrs, @call_allowed_fields)
|> validate_required(@call_required_fields)
# TODO consider removing
|> validate_call_error_or_result()
|> check_constraint(:call_type, message: ~S|can't be blank when type is 'call'|, name: :call_has_call_type)
|> check_constraint(:input, message: ~S|can't be blank when type is 'call'|, name: :call_has_input)
@ -435,6 +428,7 @@ defmodule Explorer.Chain.InternalTransaction do
changeset
|> cast(attrs, @create_allowed_fields)
|> validate_required(@create_required_fields)
# TODO consider removing
|> validate_create_error_or_result()
|> check_constraint(:init, message: ~S|can't be blank when type is 'create'|, name: :create_has_init)
|> foreign_key_constraint(:transaction_hash)
@ -481,8 +475,14 @@ defmodule Explorer.Chain.InternalTransaction do
# Validates that :call `type` changeset either has an `error` or both `gas_used` and `output`
defp validate_call_error_or_result(changeset) do
case get_field(changeset, :error) do
nil -> validate_required(changeset, [:gas_used, :output], message: "can't be blank for successful call")
_ -> validate_disallowed(changeset, [:output], message: "can't be present for failed call")
nil ->
validate_required(changeset, [:gas_used, :output], message: "can't be blank for successful call")
_ ->
changeset
|> delete_change(:gas_used)
|> delete_change(:output)
|> validate_disallowed([:output], message: "can't be present for failed call")
end
end
@ -492,8 +492,15 @@ defmodule Explorer.Chain.InternalTransaction do
# `:created_contract_address_hash`
defp validate_create_error_or_result(changeset) do
case get_field(changeset, :error) do
nil -> validate_required(changeset, @create_success_fields, message: "can't be blank for successful create")
_ -> validate_disallowed(changeset, @create_success_fields, message: "can't be present for failed create")
nil ->
validate_required(changeset, @create_success_fields, message: "can't be blank for successful create")
_ ->
changeset
|> delete_change(:created_contract_code)
|> delete_change(:created_contract_address_hash)
|> delete_change(:gas_used)
|> validate_disallowed(@create_success_fields, message: "can't be present for failed create")
end
end

@ -141,6 +141,7 @@ defmodule Explorer.Chain.Transaction.Schema do
field(:status, Status)
field(:v, :decimal)
field(:value, Wei)
# TODO change to Data.t(), convert current hex-string values, prune all non-hex ones
field(:revert_reason, :string)
field(:max_priority_fee_per_gas, Wei)
field(:max_fee_per_gas, Wei)
@ -616,22 +617,52 @@ defmodule Explorer.Chain.Transaction do
process_hex_revert_reason(hex, transaction, options)
end
defp process_hex_revert_reason(hex_revert_reason, %__MODULE__{to_address: smart_contract, hash: hash}, options) do
case Integer.parse(hex_revert_reason, 16) do
{number, ""} ->
binary_revert_reason = :binary.encode_unsigned(number)
{result, _, _} =
decoded_input_data(
%Transaction{
to_address: smart_contract,
hash: hash,
input: %Data{bytes: binary_revert_reason}
},
options
)
@default_error_abi [
%{
"inputs" => [
%{
"name" => "reason",
"type" => "string"
}
],
"name" => "Error",
"type" => "error"
},
%{
"inputs" => [
%{
"name" => "errorCode",
"type" => "uint256"
}
],
"name" => "Panic",
"type" => "error"
}
]
result
defp process_hex_revert_reason(hex_revert_reason, %__MODULE__{to_address: smart_contract, hash: hash}, options) do
case Base.decode16(hex_revert_reason, case: :mixed) do
{:ok, binary_revert_reason} ->
case find_and_decode(@default_error_abi, binary_revert_reason, hash) do
{:ok, {selector, values}} ->
{:ok, mapping} = selector_mapping(selector, values, hash)
identifier = Base.encode16(selector.method_id, case: :lower)
text = function_call(selector.function, mapping)
{:ok, identifier, text, mapping}
_ ->
{result, _, _} =
decoded_input_data(
%Transaction{
to_address: smart_contract,
hash: hash,
input: %Data{bytes: binary_revert_reason}
},
options
)
result
end
_ ->
hex_revert_reason

@ -4217,8 +4217,12 @@ defmodule Explorer.ChainTest do
describe "transaction_to_revert_reason/1" do
test "returns correct revert_reason from DB" do
transaction = insert(:transaction, revert_reason: "No credit of that type")
assert Chain.transaction_to_revert_reason(transaction) == "No credit of that type"
# Error("No credit of that type")
hex_reason =
"0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000164e6f20637265646974206f662074686174207479706500000000000000000000"
transaction = insert(:transaction, revert_reason: hex_reason)
assert Chain.transaction_to_revert_reason(transaction) == hex_reason
end
test "returns correct revert_reason from the archive node" do
@ -4231,15 +4235,57 @@ defmodule Explorer.ChainTest do
)
|> with_block(insert(:block, number: 1))
# Error("No credit of that type")
hex_reason =
"0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000164e6f20637265646974206f662074686174207479706500000000000000000000"
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn _json, [] ->
{:error, %{code: -32015, message: "VM execution error.", data: "revert: No credit of that type"}}
{:ok,
[
%{
id: 0,
result: %{
"output" => "0x",
"stateDiff" => nil,
"trace" => [
%{
"action" => %{
"callType" => "call",
"from" => "0x6a17ca3bbf83764791f4a9f2b4dbbaebbc8b3e0d",
"gas" => "0x5208",
"input" => "0x01",
"to" => "0x7ed1e469fcb3ee19c0366d829e291451be638e59",
"value" => "0x86b3"
},
"error" => "Reverted",
"result" => %{
"gasUsed" => "0x5208",
"output" => hex_reason
},
"subtraces" => 0,
"traceAddress" => [],
"type" => "call"
}
],
"transactionHash" => "0xdf5574290913659a1ac404ccf2d216c40587f819400a52405b081dda728ac120",
"vmTrace" => nil
}
}
]}
end
)
assert Chain.transaction_to_revert_reason(transaction) == "No credit of that type"
assert Chain.transaction_to_revert_reason(transaction) == hex_reason
assert Transaction.decoded_revert_reason(transaction, hex_reason) == {
:ok,
"08c379a0",
"Error(string reason)",
[{"reason", "string", "No credit of that type"}]
}
end
end
end

Loading…
Cancel
Save