diff --git a/CHANGELOG.md b/CHANGELOG.md index f9e00a2fbb..8227c01b41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Features - [#3154](https://github.com/poanetwork/blockscout/pull/3154) - Support of Hyperledger Besu client +- [#3153](https://github.com/poanetwork/blockscout/pull/3153) - Proxy contracts: logs decoding using implementation ABI +- [#3153](https://github.com/poanetwork/blockscout/pull/3153) - Proxy contracts: methods decoding using implementation ABI - [#3149](https://github.com/poanetwork/blockscout/pull/3149) - Display and store revert reason of tx on demand at transaction details page and at gettxinfo API endpoint. ### Fixes diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index bd520b4e70..b0a7f06de9 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -74,6 +74,7 @@ defmodule Explorer.Chain do alias Explorer.Counters.{AddressesCounter, AddressesWithBalanceCounter} alias Explorer.Market.MarketHistoryCache alias Explorer.{PagingOptions, Repo} + alias Explorer.SmartContract.Reader alias Dataloader.Ecto, as: DataloaderEcto @@ -4337,6 +4338,56 @@ defmodule Explorer.Chain do end end + def combine_proxy_implementation_abi(address_hash, abi) when not is_nil(abi) do + implementation_method_abi = + abi + |> Enum.find(fn method -> + Map.get(method, "name") == "implementation" + end) + + implementation_abi = + if implementation_method_abi do + implementation_address = + case Reader.query_contract(address_hash, abi, %{ + "implementation" => [] + }) do + %{"implementation" => {:ok, [result]}} -> result + _ -> nil + end + + if implementation_address do + implementation_address_hash_string = "0x" <> Base.encode16(implementation_address, case: :lower) + + case Chain.string_to_address_hash(implementation_address_hash_string) do + {:ok, implementation_address_hash} -> + implementation_smart_contract = + implementation_address_hash + |> Chain.address_hash_to_smart_contract() + + if implementation_smart_contract do + implementation_smart_contract + |> Map.get(:abi) + else + [] + end + + _ -> + [] + end + else + [] + end + else + [] + end + + if Enum.empty?(implementation_abi), do: abi, else: implementation_abi ++ abi + end + + def combine_proxy_implementation_abi(_, abi) when is_nil(abi) do + [] + end + defp format_tx_first_trace(first_trace, block_hash, json_rpc_named_arguments) do {:ok, to_address_hash} = if Map.has_key?(first_trace, :to_address_hash) do diff --git a/apps/explorer/lib/explorer/chain/log.ex b/apps/explorer/lib/explorer/chain/log.ex index 27899303f3..9592f0b2dd 100644 --- a/apps/explorer/lib/explorer/chain/log.ex +++ b/apps/explorer/lib/explorer/chain/log.ex @@ -6,8 +6,8 @@ defmodule Explorer.Chain.Log do require Logger alias ABI.{Event, FunctionSelector} + alias Explorer.{Chain, Repo} alias Explorer.Chain.{Address, Block, ContractMethod, Data, Hash, Transaction} - alias Explorer.Repo @required_attrs ~w(address_hash data block_hash index transaction_hash)a @optional_attrs ~w(first_topic second_topic third_topic fourth_topic type block_number)a @@ -121,8 +121,11 @@ defmodule Explorer.Chain.Log do """ def decode(_log, %Transaction{to_address: nil}), do: {:error, :no_to_address} - def decode(log, transaction = %Transaction{to_address: %{smart_contract: %{abi: abi}}}) when not is_nil(abi) do - with {:ok, selector, mapping} <- find_and_decode(abi, log, transaction), + def decode(log, transaction = %Transaction{to_address: %{smart_contract: %{abi: abi, address_hash: address_hash}}}) + when not is_nil(abi) do + full_abi = Chain.combine_proxy_implementation_abi(address_hash, abi) + + with {:ok, selector, mapping} <- find_and_decode(full_abi, log, transaction), identifier <- Base.encode16(selector.method_id, case: :lower), text <- function_call(selector.function, mapping), do: {:ok, identifier, text, mapping} diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex index 9d60d58edd..e4518c7349 100644 --- a/apps/explorer/lib/explorer/chain/transaction.ex +++ b/apps/explorer/lib/explorer/chain/transaction.ex @@ -11,6 +11,8 @@ defmodule Explorer.Chain.Transaction do alias Ecto.Changeset + alias Explorer.{Chain, Repo} + alias Explorer.Chain.{ Address, Block, @@ -26,7 +28,6 @@ defmodule Explorer.Chain.Transaction do } alias Explorer.Chain.Transaction.{Fork, Status} - alias Explorer.Repo @optional_attrs ~w(block_hash block_number created_contract_address_hash cumulative_gas_used earliest_processing_start error gas_used index created_contract_code_indexed_at status @@ -423,7 +424,7 @@ defmodule Explorer.Chain.Transaction do candidates_query |> Repo.all() |> Enum.flat_map(fn candidate -> - case do_decoded_input_data(data, [candidate.abi], hash) do + case do_decoded_input_data(data, [candidate.abi], nil, hash) do {:ok, _, _, _} = decoded -> [decoded] _ -> [] end @@ -436,12 +437,18 @@ defmodule Explorer.Chain.Transaction do {:error, :contract_not_verified, []} end - def decoded_input_data(%__MODULE__{input: %{bytes: data}, to_address: %{smart_contract: %{abi: abi}}, hash: hash}) do - do_decoded_input_data(data, abi, hash) + def decoded_input_data(%__MODULE__{ + input: %{bytes: data}, + to_address: %{smart_contract: %{abi: abi, address_hash: address_hash}}, + hash: hash + }) do + do_decoded_input_data(data, abi, address_hash, hash) end - defp do_decoded_input_data(data, abi, hash) do - with {:ok, {selector, values}} <- find_and_decode(abi, data, hash), + defp do_decoded_input_data(data, abi, address_hash, hash) do + full_abi = Chain.combine_proxy_implementation_abi(address_hash, abi) + + with {:ok, {selector, values}} <- find_and_decode(full_abi, data, hash), {:ok, mapping} <- selector_mapping(selector, values, hash), identifier <- Base.encode16(selector.method_id, case: :lower), text <- function_call(selector.function, mapping), diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index ed6d37aaa4..e374ab492c 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -5193,4 +5193,176 @@ defmodule Explorer.ChainTest do assert Chain.transaction_to_revert_reason(transaction) == "No credit of that type" end end + + describe "combine_proxy_implementation_abi/2" do + @proxy_abi [ + %{ + "type" => "function", + "stateMutability" => "nonpayable", + "payable" => false, + "outputs" => [%{"type" => "bool", "name" => ""}], + "name" => "upgradeTo", + "inputs" => [%{"type" => "address", "name" => "newImplementation"}], + "constant" => false + }, + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "uint256", "name" => ""}], + "name" => "version", + "inputs" => [], + "constant" => true + }, + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "address", "name" => ""}], + "name" => "implementation", + "inputs" => [], + "constant" => true + }, + %{ + "type" => "function", + "stateMutability" => "nonpayable", + "payable" => false, + "outputs" => [], + "name" => "renounceOwnership", + "inputs" => [], + "constant" => false + }, + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "address", "name" => ""}], + "name" => "getOwner", + "inputs" => [], + "constant" => true + }, + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "address", "name" => ""}], + "name" => "getProxyStorage", + "inputs" => [], + "constant" => true + }, + %{ + "type" => "function", + "stateMutability" => "nonpayable", + "payable" => false, + "outputs" => [], + "name" => "transferOwnership", + "inputs" => [%{"type" => "address", "name" => "_newOwner"}], + "constant" => false + }, + %{ + "type" => "constructor", + "stateMutability" => "nonpayable", + "payable" => false, + "inputs" => [ + %{"type" => "address", "name" => "_proxyStorage"}, + %{"type" => "address", "name" => "_implementationAddress"} + ] + }, + %{"type" => "fallback", "stateMutability" => "nonpayable", "payable" => false}, + %{ + "type" => "event", + "name" => "Upgraded", + "inputs" => [ + %{"type" => "uint256", "name" => "version", "indexed" => false}, + %{"type" => "address", "name" => "implementation", "indexed" => true} + ], + "anonymous" => false + }, + %{ + "type" => "event", + "name" => "OwnershipRenounced", + "inputs" => [%{"type" => "address", "name" => "previousOwner", "indexed" => true}], + "anonymous" => false + }, + %{ + "type" => "event", + "name" => "OwnershipTransferred", + "inputs" => [ + %{"type" => "address", "name" => "previousOwner", "indexed" => true}, + %{"type" => "address", "name" => "newOwner", "indexed" => true} + ], + "anonymous" => false + } + ] + + @implementation_abi [ + %{ + "constant" => false, + "inputs" => [%{"name" => "x", "type" => "uint256"}], + "name" => "set", + "outputs" => [], + "payable" => false, + "stateMutability" => "nonpayable", + "type" => "function" + }, + %{ + "constant" => true, + "inputs" => [], + "name" => "get", + "outputs" => [%{"name" => "", "type" => "uint256"}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + } + ] + + test "returns empty [] abi if proxy abi is null" do + proxy_contract_address = insert(:contract_address) + assert Chain.combine_proxy_implementation_abi(proxy_contract_address, nil) == [] + end + + test "returns [] abi for unverified proxy" do + proxy_contract_address = insert(:contract_address) + assert Chain.combine_proxy_implementation_abi(proxy_contract_address, []) == [] + end + + test "returns proxy abi if implementation is not verified" do + proxy_contract_address = insert(:contract_address) + insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi) + assert Chain.combine_proxy_implementation_abi(proxy_contract_address, @proxy_abi) == @proxy_abi + end + + test "returns proxy + implementation abi if implementation is verified" do + proxy_contract_address = insert(:contract_address) + insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi) + + implementation_contract_address = insert(:contract_address) + insert(:smart_contract, address_hash: implementation_contract_address.hash, abi: @implementation_abi) + + implementation_contract_address_hash_string = + Base.encode16(implementation_contract_address.hash.bytes, case: :lower) + + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn [%{id: id, method: _, params: [%{data: _, to: _}, _]}], _options -> + {:ok, + [ + %{ + id: id, + jsonrpc: "2.0", + result: "0x000000000000000000000000" <> implementation_contract_address_hash_string + } + ]} + end + ) + + combined_abi = Chain.combine_proxy_implementation_abi(proxy_contract_address.hash, @proxy_abi) + + assert Enum.any?(@proxy_abi, fn el -> el == Enum.at(@implementation_abi, 0) end) == false + assert Enum.any?(@proxy_abi, fn el -> el == Enum.at(@implementation_abi, 1) end) == false + assert Enum.any?(combined_abi, fn el -> el == Enum.at(@implementation_abi, 0) end) == true + assert Enum.any?(combined_abi, fn el -> el == Enum.at(@implementation_abi, 1) end) == true + end + end end