diff --git a/CHANGELOG.md b/CHANGELOG.md index c51c860b26..c935a61676 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## Current ### Features +- [#3420](https://github.com/poanetwork/blockscout/pull/3420) - Enable read/write proxy tabs for Gnosis safe proxy contract - [#3411](https://github.com/poanetwork/blockscout/pull/3411) - Circles UBI theme - [#3406](https://github.com/poanetwork/blockscout/pull/3406), [#3409](https://github.com/poanetwork/blockscout/pull/3409) - Adding mp4 files support for NFTs - [#3398](https://github.com/poanetwork/blockscout/pull/3398) - Collect and display gas usage per day at the main page diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex index 5b001689ee..ee84c96e89 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex @@ -89,7 +89,7 @@ defmodule BlockScoutWeb.SmartContractController do with true <- ajax?(conn), {:ok, address_hash} <- Chain.string_to_address_hash(params["id"]), {:ok, address} <- Chain.find_contract_address(address_hash, address_options, true) do - contract_type = if Chain.is_proxy_contract?(address.smart_contract.abi), do: :proxy, else: :regular + contract_type = if Chain.proxy_contract?(address.smart_contract.abi), do: :proxy, else: :regular outputs = Reader.query_function( diff --git a/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_functions.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_functions.html.eex index 6fc45d0cc4..976892eec0 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_functions.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_functions.html.eex @@ -44,7 +44,7 @@ to: address_contract_path(@conn, :index, metadata_for_verification.address_hash) → - <%= if queryable?(function["inputs"]) || writeable?(function) do %> + <%= if queryable?(function["inputs"]) || writable?(function) do %>
<%= for status <- ["error", "warning", "success", "question"] do @@ -52,7 +52,7 @@ to: address_contract_path(@conn, :index, metadata_for_verification.address_hash) end %> <%= render BlockScoutWeb.SmartContractView, "_pending_contract_write.html" %> -
+ @@ -70,7 +70,7 @@ to: address_contract_path(@conn, :index, metadata_for_verification.address_hash)
<% end %> - + <%= if outputs?(function["outputs"]) do %> diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_view.ex index 3a07075148..0d77a14009 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/address_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/address_view.ex @@ -235,7 +235,7 @@ defmodule BlockScoutWeb.AddressView do def smart_contract_with_read_only_functions?(%Address{smart_contract: nil}), do: false def smart_contract_is_proxy?(%Address{smart_contract: %SmartContract{}} = address) do - Chain.is_proxy_contract?(address.smart_contract.abi) + Chain.proxy_contract?(address.smart_contract.abi) end def smart_contract_is_proxy?(%Address{smart_contract: nil}), do: false diff --git a/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex index 207c6eb8eb..dcf7a98ee2 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex @@ -7,12 +7,12 @@ defmodule BlockScoutWeb.SmartContractView do def queryable?(inputs) when is_nil(inputs), do: false - def writeable?(function) when not is_nil(function), + def writable?(function) when not is_nil(function), do: !constructor?(function) && !event?(function) && (payable?(function) || nonpayable?(function)) - def writeable?(function) when is_nil(function), do: false + def writable?(function) when is_nil(function), do: false def outputs?(outputs) when not is_nil(outputs), do: Enum.any?(outputs) diff --git a/apps/block_scout_web/test/block_scout_web/views/tokens/smart_contract_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/tokens/smart_contract_view_test.exs index cc812cd41b..32be1fab47 100644 --- a/apps/block_scout_web/test/block_scout_web/views/tokens/smart_contract_view_test.exs +++ b/apps/block_scout_web/test/block_scout_web/views/tokens/smart_contract_view_test.exs @@ -17,7 +17,7 @@ defmodule BlockScoutWeb.SmartContractViewTest do end end - describe "writeable?" do + describe "writable?" do test "returns true when there is write function" do function = %{ "type" => "function", @@ -29,7 +29,7 @@ defmodule BlockScoutWeb.SmartContractViewTest do "constant" => false } - assert SmartContractView.writeable?(function) + assert SmartContractView.writable?(function) end test "returns false when it is not a write function" do @@ -43,19 +43,19 @@ defmodule BlockScoutWeb.SmartContractViewTest do "constant" => true } - refute SmartContractView.writeable?(function) + refute SmartContractView.writable?(function) end test "returns false when there is no function" do function = %{} - refute SmartContractView.writeable?(function) + refute SmartContractView.writable?(function) end test "returns false when there function is nil" do function = nil - refute SmartContractView.writeable?(function) + refute SmartContractView.writable?(function) end end diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index e7b4838077..d6987e769c 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -5315,17 +5315,18 @@ defmodule Explorer.Chain do [] end - def is_proxy_contract?(abi) when not is_nil(abi) do + def proxy_contract?(abi) when not is_nil(abi) do implementation_method_abi = abi |> Enum.find(fn method -> - Map.get(method, "name") == "implementation" + Map.get(method, "name") == "implementation" || + master_copy_pattern?(method) end) if implementation_method_abi, do: true, else: false end - def is_proxy_contract?(abi) when is_nil(abi) do + def proxy_contract?(abi) when is_nil(abi) do false end @@ -5337,47 +5338,29 @@ defmodule Explorer.Chain do Map.get(method, "name") == "implementation" end) - implementation_method_abi_state_mutability = Map.get(implementation_method_abi, "stateMutability") + implementation_method_abi_state_mutability = + implementation_method_abi && Map.get(implementation_method_abi, "stateMutability") + is_eip1967 = if implementation_method_abi_state_mutability == "nonpayable", do: true, else: false - if is_eip1967 do - json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) + master_copy_method_abi = + abi + |> Enum.find(fn method -> + master_copy_pattern?(method) + end) - # https://eips.ethereum.org/EIPS/eip-1967 - eip_1967_implementation_storage_pointer = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc" + cond do + is_eip1967 -> + get_implementation_address_hash_eip_1967(proxy_address_hash) - {:ok, implementation_address} = - Contract.eth_get_storage_at_request( - proxy_address_hash, - eip_1967_implementation_storage_pointer, - nil, - json_rpc_named_arguments - ) + implementation_method_abi -> + get_implementation_address_hash_basic(proxy_address_hash, abi) - if String.length(implementation_address) > 42 do - "0x" <> String.slice(implementation_address, -40, 40) - else - implementation_address - end - else - # 5c60da1b = keccak256(implementation()) - implementation_address = - case Reader.query_contract(proxy_address_hash, abi, %{ - "5c60da1b" => [] - }) do - %{"5c60da1b" => {:ok, [result]}} -> result - _ -> nil - end + master_copy_method_abi -> + get_implementation_address_hash_from_master_copy_pattern(proxy_address_hash) - if implementation_address do - if String.starts_with?(implementation_address, "0x") do - implementation_address - else - "0x" <> Base.encode16(implementation_address, case: :lower) - end - else + true -> nil - end end end @@ -5385,6 +5368,91 @@ defmodule Explorer.Chain do nil end + defp get_implementation_address_hash_eip_1967(proxy_address_hash) do + json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) + + # https://eips.ethereum.org/EIPS/eip-1967 + eip_1967_implementation_storage_pointer = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc" + + {:ok, implementation_address} = + Contract.eth_get_storage_at_request( + proxy_address_hash, + eip_1967_implementation_storage_pointer, + nil, + json_rpc_named_arguments + ) + + abi_decode_address_output(implementation_address) + end + + defp get_implementation_address_hash_basic(proxy_address_hash, abi) do + # 5c60da1b = keccak256(implementation()) + implementation_address = + case Reader.query_contract(proxy_address_hash, abi, %{ + "5c60da1b" => [] + }) do + %{"5c60da1b" => {:ok, [result]}} -> result + _ -> nil + end + + address_to_hex(implementation_address) + end + + defp get_implementation_address_hash_from_master_copy_pattern(proxy_address_hash) do + json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) + + master_copy_storage_pointer = "0x0" + + {:ok, implementation_address} = + Contract.eth_get_storage_at_request( + proxy_address_hash, + master_copy_storage_pointer, + nil, + json_rpc_named_arguments + ) + + abi_decode_address_output(implementation_address) + end + + defp master_copy_pattern?(method) do + Map.get(method, "type") == "constructor" && + method + |> Enum.find(fn item -> + case item do + {"inputs", inputs} -> + master_copy_input?(inputs) + + _ -> + false + end + end) + end + + defp master_copy_input?(inputs) do + inputs + |> Enum.find(fn input -> + Map.get(input, "name") == "_masterCopy" + end) + end + + defp abi_decode_address_output(address) do + if String.length(address) > 42 do + "0x" <> String.slice(address, -40, 40) + else + address + end + end + + defp address_to_hex(address) do + if address do + if String.starts_with?(address, "0x") do + address + else + "0x" <> Base.encode16(address, case: :lower) + end + end + end + def get_implementation_abi(implementation_address_hash_string) when not is_nil(implementation_address_hash_string) do case Chain.string_to_address_hash(implementation_address_hash_string) do {:ok, implementation_address_hash} ->