diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex index 936b79b2bf..9227a585d7 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex @@ -22,8 +22,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do %{"items" => Enum.map(smart_contracts, &prepare_smart_contract_for_list/1), "next_page_params" => next_page_params} end - def render("smart_contract.json", %{address: address}) do - prepare_smart_contract(address) + def render("smart_contract.json", %{address: address, conn: conn}) do + prepare_smart_contract(address, conn) end def render("read_functions.json", %{functions: functions}) do @@ -146,12 +146,18 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do defp prepare_output(output), do: output # credo:disable-for-next-line - def prepare_smart_contract(%Address{smart_contract: %SmartContract{} = smart_contract} = address) do + def prepare_smart_contract(%Address{smart_contract: %SmartContract{} = smart_contract} = address, conn) do minimal_proxy_template = EIP1167.get_implementation_address(address.hash, @api_true) bytecode_twin = SmartContract.get_address_verified_twin_contract(address.hash, @api_true) metadata_for_verification = minimal_proxy_template || bytecode_twin.verified_contract smart_contract_verified = AddressView.smart_contract_verified?(address) fully_verified = SmartContract.verified_with_full_match?(address.hash, @api_true) + write_methods? = AddressView.smart_contract_with_write_functions?(address) + + is_proxy = AddressView.smart_contract_is_proxy?(address, @api_true) + + read_custom_abi? = AddressView.has_address_custom_abi_with_read_functions?(conn, address.hash) + write_custom_abi? = AddressView.has_address_custom_abi_with_write_functions?(conn, address.hash) additional_sources = additional_sources(smart_contract, smart_contract_verified, minimal_proxy_template, bytecode_twin) @@ -170,6 +176,12 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do "is_verified_via_eth_bytecode_db" => address.smart_contract.verified_via_eth_bytecode_db, "is_verified_via_verifier_alliance" => address.smart_contract.verified_via_verifier_alliance, "is_vyper_contract" => target_contract.is_vyper_contract, + "has_custom_methods_read" => read_custom_abi?, + "has_custom_methods_write" => write_custom_abi?, + "has_methods_read" => AddressView.smart_contract_with_read_only_functions?(address), + "has_methods_write" => write_methods?, + "has_methods_read_proxy" => is_proxy, + "has_methods_write_proxy" => is_proxy && write_methods?, "minimal_proxy_address_hash" => minimal_proxy_template && Address.checksum(metadata_for_verification.address_hash), "sourcify_repo_url" => @@ -201,8 +213,15 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do |> Map.merge(bytecode_info(address)) end - def prepare_smart_contract(address) do - bytecode_info(address) + def prepare_smart_contract(address, conn) do + read_custom_abi? = AddressView.has_address_custom_abi_with_read_functions?(conn, address.hash) + write_custom_abi? = AddressView.has_address_custom_abi_with_write_functions?(conn, address.hash) + + %{ + "has_custom_methods_read" => read_custom_abi?, + "has_custom_methods_write" => write_custom_abi? + } + |> Map.merge(bytecode_info(address)) end @doc """ diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs index 385455ba5a..f35dab08af 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs @@ -36,6 +36,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do assert response == %{ + "has_custom_methods_read" => false, + "has_custom_methods_write" => false, "is_self_destructed" => false, "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => nil @@ -53,6 +55,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do assert response == %{ + "has_custom_methods_read" => false, + "has_custom_methods_write" => false, "is_self_destructed" => false, "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => @@ -86,6 +90,12 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "is_fully_verified" => true, "is_verified_via_sourcify" => target_contract.verified_via_sourcify, "is_vyper_contract" => target_contract.is_vyper_contract, + "has_methods_read" => true, + "has_methods_write" => true, + "has_methods_read_proxy" => true, + "has_methods_write_proxy" => true, + "has_custom_methods_read" => false, + "has_custom_methods_write" => false, "minimal_proxy_address_hash" => nil, "sourcify_repo_url" => if(target_contract.verified_via_sourcify, @@ -117,6 +127,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "license_type" => "none" } + get_eip1967_implementation_non_zero_address() + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(target_contract.address_hash)}") response = json_response(request, 200) @@ -177,6 +189,12 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "is_fully_verified" => true, "is_verified_via_sourcify" => target_contract.verified_via_sourcify, "is_vyper_contract" => target_contract.is_vyper_contract, + "has_methods_read" => true, + "has_methods_read_proxy" => false, + "has_methods_write" => true, + "has_methods_write_proxy" => false, + "has_custom_methods_read" => false, + "has_custom_methods_write" => false, "minimal_proxy_address_hash" => nil, "sourcify_repo_url" => if(target_contract.verified_via_sourcify, @@ -211,6 +229,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "license_type" => "gnu_agpl_v3" } + get_eip1967_implementation_error_response() + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(target_contract.address_hash)}") response = json_response(request, 200) @@ -279,6 +299,12 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "is_fully_verified" => false, "is_verified_via_sourcify" => false, "is_vyper_contract" => target_contract.is_vyper_contract, + "has_methods_read" => true, + "has_methods_write" => true, + "has_methods_read_proxy" => false, + "has_methods_write_proxy" => false, + "has_custom_methods_read" => false, + "has_custom_methods_write" => false, "minimal_proxy_address_hash" => nil, "sourcify_repo_url" => nil, "can_be_visualized_via_sol2uml" => false, @@ -307,6 +333,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "license_type" => "none" } + get_eip1967_implementation_error_response() + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") response = json_response(request, 200) @@ -390,6 +418,12 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "is_fully_verified" => false, "is_verified_via_sourcify" => false, "is_vyper_contract" => implementation_contract.is_vyper_contract, + "has_methods_read" => true, + "has_methods_write" => true, + "has_methods_read_proxy" => true, + "has_methods_write_proxy" => true, + "has_custom_methods_read" => false, + "has_custom_methods_write" => false, "minimal_proxy_address_hash" => Address.checksum("0x" <> implementation_contract_address_hash_string), "sourcify_repo_url" => nil, "can_be_visualized_via_sol2uml" => false, @@ -475,6 +509,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do Conn.resp(conn, 200, eth_bytecode_response) end) + get_eip1967_implementation_error_response() + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") assert_receive %Phoenix.Socket.Message{ @@ -495,6 +531,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do assert response == %{ + "has_custom_methods_read" => false, + "has_custom_methods_write" => false, "is_self_destructed" => false, "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => @@ -554,6 +592,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do Conn.resp(conn, 200, eth_bytecode_response) end) + get_eip1967_implementation_error_response() + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") assert_receive %Phoenix.Socket.Message{ @@ -574,6 +614,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do assert response == %{ + "has_custom_methods_read" => false, + "has_custom_methods_write" => false, "is_self_destructed" => false, "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => @@ -700,12 +742,16 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do assert response == %{ + "has_custom_methods_read" => false, + "has_custom_methods_write" => false, "is_self_destructed" => false, "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029" } + get_eip1967_implementation_error_response() + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") assert response = json_response(request, 200) assert %{"is_verified" => true} = response @@ -763,6 +809,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do Conn.resp(conn, 200, eth_bytecode_response) end) + get_eip1967_implementation_error_response() + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") assert_receive %Phoenix.Socket.Message{ @@ -783,6 +831,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do assert response == %{ + "has_custom_methods_read" => false, + "has_custom_methods_write" => false, "is_self_destructed" => false, "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => @@ -869,6 +919,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do Conn.resp(conn, 200, eth_bytecode_response) end) + get_eip1967_implementation_non_zero_address() + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") assert_receive %Phoenix.Socket.Message{ @@ -889,6 +941,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do assert response == %{ + "has_custom_methods_read" => false, + "has_custom_methods_write" => false, "is_self_destructed" => false, "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => @@ -980,6 +1034,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do Conn.resp(conn, 200, eth_bytecode_response) end) + get_eip1967_implementation_non_zero_address() + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") assert_receive %Phoenix.Socket.Message{ @@ -1000,6 +1056,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do assert response == %{ + "has_custom_methods_read" => false, + "has_custom_methods_write" => false, "is_self_destructed" => false, "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => @@ -1091,6 +1149,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do Conn.resp(conn, 200, eth_bytecode_response) end) + get_eip1967_implementation_non_zero_address() + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") assert_receive %Phoenix.Socket.Message{ @@ -1111,6 +1171,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do assert response == %{ + "has_custom_methods_read" => false, + "has_custom_methods_write" => false, "is_self_destructed" => false, "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => @@ -3219,4 +3281,107 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do {:ok, "0x000000000000000000000000#{address_hash |> to_string() |> String.replace("0x", "")}"} end) end + + def get_eip1967_implementation_non_zero_address do + expect(EthereumJSONRPC.Mox, :json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} + end) + |> expect(:json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} + end) + |> expect(:json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000001"} + end) + end + + def get_eip1967_implementation_error_response do + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", + "latest" + ] + }, + _options -> + {:error, "error"} + end) + |> mock_empty_beacon_storage_pointer_request() + |> mock_empty_oz_storage_pointer_request() + |> mock_empty_eip_1822_storage_pointer_request() + end + + defp mock_empty_beacon_storage_pointer_request(mox) do + expect(mox, :json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} + end) + end + + defp mock_empty_eip_1822_storage_pointer_request(mox) do + expect(mox, :json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} + end) + end + + defp mock_empty_oz_storage_pointer_request(mox) do + expect(mox, :json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3", + "latest" + ] + }, + _options -> + {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"} + end) + end end