diff --git a/apps/block_scout_web/lib/block_scout_web/api_router.ex b/apps/block_scout_web/lib/block_scout_web/api_router.ex index 5ac16a5e5a..ef936d83aa 100644 --- a/apps/block_scout_web/lib/block_scout_web/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/api_router.ex @@ -28,6 +28,12 @@ defmodule BlockScoutWeb.ApiRouter do pipeline :api_v2 do plug(CheckApiV2) plug(:fetch_session) + plug(:protect_from_forgery) + end + + pipeline :api_v2_no_forgery_protect do + plug(CheckApiV2) + plug(:fetch_session) end alias BlockScoutWeb.Account.Api.V1.{TagsController, UserController} @@ -135,7 +141,6 @@ defmodule BlockScoutWeb.ApiRouter do get("/:address_hash/methods-write", V2.SmartContractController, :methods_write) get("/:address_hash/methods-read-proxy", V2.SmartContractController, :methods_read_proxy) get("/:address_hash/methods-write-proxy", V2.SmartContractController, :methods_write_proxy) - post("/:address_hash/query-read-method", V2.SmartContractController, :query_read_method) end scope "/tokens" do @@ -161,6 +166,15 @@ defmodule BlockScoutWeb.ApiRouter do end end + scope "/v2", as: :api_v2 do + pipe_through(:api) + pipe_through(:api_v2_no_forgery_protect) + + scope "/smart-contracts" do + post("/:address_hash/query-read-method", BlockScoutWeb.API.V2.SmartContractController, :query_read_method) + end + end + scope "/v1", as: :api_v1 do pipe_through(:api) alias BlockScoutWeb.API.{EthRPC, RPC, V1} 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 da8981b452..db8b575a02 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 @@ -99,6 +99,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do end end + # credo:disable-for-next-line def prepare_smart_contract(address) do minimal_proxy_template = Chain.get_minimal_proxy_template(address.hash) 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 9ff6a31597..d9e5b2ccd5 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 @@ -658,6 +658,502 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do end end + describe "/smart-contracts/{address_hash}/methods-read-proxy" do + test "get 404 on non existing SC", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/smart-contracts/#{address.hash}/methods-read-proxy") + + assert %{"message" => "Not found"} = json_response(request, 404) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/smart-contracts/0x/methods-read-proxy") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get read-methods", %{conn: conn} do + abi = [ + %{ + "type" => "function", + "stateMutability" => "view", + "outputs" => [%{"type" => "address", "name" => "", "internalType" => "address"}], + "name" => "getCaller", + "inputs" => [] + }, + %{ + "type" => "function", + "stateMutability" => "view", + "outputs" => [%{"type" => "bool", "name" => "", "internalType" => "bool"}], + "name" => "isWhitelist", + "inputs" => [%{"type" => "address", "name" => "_address", "internalType" => "address"}] + }, + %{ + "type" => "function", + "stateMutability" => "nonpayable", + "outputs" => [], + "name" => "disableWhitelist", + "inputs" => [%{"type" => "bool", "name" => "disable", "internalType" => "bool"}] + } + ] + + target_contract = insert(:smart_contract, abi: abi) + blockchain_get_code_mock() + + expect(EthereumJSONRPC.Mox, :json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", + "latest" + ] + }, + _options -> + {:ok, "0x000000000000000000000000#{target_contract.address_hash |> to_string() |> String.replace("0x", "")}"} + end) + + address_hash = to_string(target_contract.address_hash) + + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn [%{id: id, method: "eth_call", params: [%{to: address_hash}, _]}], opts -> + {:ok, + [ + %{ + id: id, + jsonrpc: "2.0", + result: "0x000000000000000000000000fffffffffffffffffffffffffffffffffffffffe" + } + ]} + end + ) + + contract = insert(:smart_contract) + request = get(conn, "/api/v2/smart-contracts/#{contract.address_hash}/methods-read-proxy") + assert response = json_response(request, 200) + + assert %{ + "type" => "function", + "stateMutability" => "view", + "outputs" => [ + %{ + "type" => "address", + "name" => "", + "internalType" => "address", + "value" => "0xfffffffffffffffffffffffffffffffffffffffe" + } + ], + "name" => "getCaller", + "inputs" => [], + "method_id" => "ab470f05" + } in response + + assert %{ + "type" => "function", + "stateMutability" => "view", + "outputs" => [%{"type" => "bool", "name" => "", "internalType" => "bool", "value" => ""}], + "name" => "isWhitelist", + "inputs" => [%{"type" => "address", "name" => "_address", "internalType" => "address"}], + "method_id" => "c683630d" + } in response + end + end + + describe "/smart-contracts/{address_hash}/query-read-method proxy" do + test "get 404 on non existing SC", %{conn: conn} do + address = build(:address) + + request = + post(conn, "/api/v2/smart-contracts/#{address.hash}/query-read-method", %{ + "contract_type" => "proxy", + "args" => ["0xfffffffffffffffffffffffffffffffffffffffe"], + "method_id" => "c683630d" + }) + + assert %{"message" => "Not found"} = json_response(request, 404) + end + + test "get 422 on invalid address", %{conn: conn} do + request = + post(conn, "/api/v2/smart-contracts/0x/query-read-method", %{ + "contract_type" => "proxy", + "args" => ["0xfffffffffffffffffffffffffffffffffffffffe"], + "method_id" => "c683630d" + }) + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "query-read-method", %{conn: conn} do + abi = [ + %{ + "type" => "function", + "stateMutability" => "view", + "outputs" => [%{"type" => "address", "name" => "", "internalType" => "address"}], + "name" => "getCaller", + "inputs" => [] + }, + %{ + "type" => "function", + "stateMutability" => "view", + "outputs" => [%{"type" => "bool", "name" => "", "internalType" => "bool"}], + "name" => "isWhitelist", + "inputs" => [%{"type" => "address", "name" => "_address", "internalType" => "address"}] + }, + %{ + "type" => "function", + "stateMutability" => "nonpayable", + "outputs" => [], + "name" => "disableWhitelist", + "inputs" => [%{"type" => "bool", "name" => "disable", "internalType" => "bool"}] + } + ] + + target_contract = insert(:smart_contract, abi: abi) + + expect(EthereumJSONRPC.Mox, :json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", + "latest" + ] + }, + _options -> + {:ok, "0x000000000000000000000000#{target_contract.address_hash |> to_string() |> String.replace("0x", "")}"} + end) + + address_hash = target_contract.address_hash |> to_string() + + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn [ + %{ + id: id, + method: "eth_call", + params: [ + %{ + data: "0xc683630d000000000000000000000000fffffffffffffffffffffffffffffffffffffffe", + to: address_hash + }, + _ + ] + } + ], + _opts -> + {:ok, + [ + %{ + id: id, + jsonrpc: "2.0", + result: "0x0000000000000000000000000000000000000000000000000000000000000001" + } + ]} + end + ) + + contract = insert(:smart_contract) + + request = + post(conn, "/api/v2/smart-contracts/#{contract.address_hash}/query-read-method", %{ + "contract_type" => "proxy", + "args" => ["0xfffffffffffffffffffffffffffffffffffffffe"], + "method_id" => "c683630d" + }) + + assert response = json_response(request, 200) + + assert %{ + "is_error" => false, + "result" => %{"names" => ["bool"], "output" => [%{"type" => "bool", "value" => true}]} + } == response + end + + test "query-read-method returns error 1", %{conn: conn} do + abi = [ + %{ + "type" => "function", + "stateMutability" => "view", + "outputs" => [%{"type" => "address", "name" => "", "internalType" => "address"}], + "name" => "getCaller", + "inputs" => [] + }, + %{ + "type" => "function", + "stateMutability" => "view", + "outputs" => [%{"type" => "bool", "name" => "", "internalType" => "bool"}], + "name" => "isWhitelist", + "inputs" => [%{"type" => "address", "name" => "_address", "internalType" => "address"}] + }, + %{ + "type" => "function", + "stateMutability" => "nonpayable", + "outputs" => [], + "name" => "disableWhitelist", + "inputs" => [%{"type" => "bool", "name" => "disable", "internalType" => "bool"}] + } + ] + + target_contract = insert(:smart_contract, abi: abi) + + expect(EthereumJSONRPC.Mox, :json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", + "latest" + ] + }, + _options -> + {:ok, "0x000000000000000000000000#{target_contract.address_hash |> to_string() |> String.replace("0x", "")}"} + end) + + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn [ + %{ + id: id, + method: "eth_call", + params: [%{data: "0xc683630d000000000000000000000000fffffffffffffffffffffffffffffffffffffffe"}, _] + } + ], + _opts -> + {:ok, [%{id: id, jsonrpc: "2.0", error: %{code: "12345", message: "Error message"}}]} + end + ) + + contract = insert(:smart_contract) + + request = + post(conn, "/api/v2/smart-contracts/#{contract.address_hash}/query-read-method", %{ + "contract_type" => "proxy", + "args" => ["0xfffffffffffffffffffffffffffffffffffffffe"], + "method_id" => "c683630d" + }) + + assert response = json_response(request, 200) + + assert %{"is_error" => true, "result" => %{"code" => "12345", "message" => "Error message"}} == response + end + + test "query-read-method returns error 2", %{conn: conn} do + abi = [ + %{ + "type" => "function", + "stateMutability" => "view", + "outputs" => [%{"type" => "address", "name" => "", "internalType" => "address"}], + "name" => "getCaller", + "inputs" => [] + }, + %{ + "type" => "function", + "stateMutability" => "view", + "outputs" => [%{"type" => "bool", "name" => "", "internalType" => "bool"}], + "name" => "isWhitelist", + "inputs" => [%{"type" => "address", "name" => "_address", "internalType" => "address"}] + }, + %{ + "type" => "function", + "stateMutability" => "nonpayable", + "outputs" => [], + "name" => "disableWhitelist", + "inputs" => [%{"type" => "bool", "name" => "disable", "internalType" => "bool"}] + } + ] + + target_contract = insert(:smart_contract, abi: abi) + + expect(EthereumJSONRPC.Mox, :json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", + "latest" + ] + }, + _options -> + {:ok, "0x000000000000000000000000#{target_contract.address_hash |> to_string() |> String.replace("0x", "")}"} + end) + + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn [ + %{ + id: id, + method: "eth_call", + params: [%{data: "0xc683630d000000000000000000000000fffffffffffffffffffffffffffffffffffffffe"}, _] + } + ], + _opts -> + {:error, {:bad_gateway, "request_url"}} + end + ) + + contract = insert(:smart_contract) + + request = + post(conn, "/api/v2/smart-contracts/#{contract.address_hash}/query-read-method", %{ + "contract_type" => "proxy", + "args" => ["0xfffffffffffffffffffffffffffffffffffffffe"], + "method_id" => "c683630d" + }) + + assert response = json_response(request, 200) + assert %{"is_error" => true, "result" => %{"error" => "Bad gateway"}} == response + end + + test "query-read-method returns error 3", %{conn: conn} do + abi = [ + %{ + "type" => "function", + "stateMutability" => "view", + "outputs" => [%{"type" => "address", "name" => "", "internalType" => "address"}], + "name" => "getCaller", + "inputs" => [] + }, + %{ + "type" => "function", + "stateMutability" => "view", + "outputs" => [%{"type" => "bool", "name" => "", "internalType" => "bool"}], + "name" => "isWhitelist", + "inputs" => [%{"type" => "address", "name" => "_address", "internalType" => "address"}] + }, + %{ + "type" => "function", + "stateMutability" => "nonpayable", + "outputs" => [], + "name" => "disableWhitelist", + "inputs" => [%{"type" => "bool", "name" => "disable", "internalType" => "bool"}] + } + ] + + target_contract = insert(:smart_contract, abi: abi) + + expect(EthereumJSONRPC.Mox, :json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", + "latest" + ] + }, + _options -> + {:ok, "0x000000000000000000000000#{target_contract.address_hash |> to_string() |> String.replace("0x", "")}"} + end) + + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn [ + %{ + id: id, + method: "eth_call", + params: [%{data: "0xc683630d000000000000000000000000fffffffffffffffffffffffffffffffffffffffe"}, _] + } + ], + _opts -> + raise FunctionClauseError + end + ) + + contract = insert(:smart_contract) + + request = + post(conn, "/api/v2/smart-contracts/#{contract.address_hash}/query-read-method", %{ + "contract_type" => "proxy", + "args" => ["0xfffffffffffffffffffffffffffffffffffffffe"], + "method_id" => "c683630d" + }) + + assert response = json_response(request, 200) + + assert %{"is_error" => true, "result" => %{"error" => "no function clause matches"}} == response + end + end + + describe "/smart-contracts/{address_hash}/methods-write-proxy" do + test "get 404 on non existing SC", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/smart-contracts/#{address.hash}/methods-write-proxy") + + assert %{"message" => "Not found"} = json_response(request, 404) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/smart-contracts/0x/methods-write-proxy") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get write-methods", %{conn: conn} do + abi = [ + %{ + "type" => "function", + "stateMutability" => "view", + "outputs" => [%{"type" => "address", "name" => "", "internalType" => "address"}], + "name" => "getCaller", + "inputs" => [] + }, + %{ + "type" => "function", + "stateMutability" => "view", + "outputs" => [%{"type" => "bool", "name" => "", "internalType" => "bool"}], + "name" => "isWhitelist", + "inputs" => [%{"type" => "address", "name" => "_address", "internalType" => "address"}] + }, + %{ + "type" => "function", + "stateMutability" => "nonpayable", + "outputs" => [], + "name" => "disableWhitelist", + "inputs" => [%{"type" => "bool", "name" => "disable", "internalType" => "bool"}] + } + ] + + target_contract = insert(:smart_contract, abi: abi) + blockchain_get_code_mock() + + expect(EthereumJSONRPC.Mox, :json_rpc, fn %{ + id: 0, + method: "eth_getStorageAt", + params: [ + _, + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", + "latest" + ] + }, + _options -> + {:ok, "0x000000000000000000000000#{target_contract.address_hash |> to_string() |> String.replace("0x", "")}"} + end) + + contract = insert(:smart_contract) + + request = get(conn, "/api/v2/smart-contracts/#{contract.address_hash}/methods-write-proxy") + assert response = json_response(request, 200) + + assert [ + %{ + "type" => "function", + "stateMutability" => "nonpayable", + "outputs" => [], + "name" => "disableWhitelist", + "inputs" => [%{"type" => "bool", "name" => "disable", "internalType" => "bool"}] + } + ] == response + end + end + defp blockchain_get_code_mock do expect( EthereumJSONRPC.Mox, @@ -709,12 +1205,4 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do end ) end - - defp debug(value, key) do - require Logger - Logger.configure(truncate: :infinity) - Logger.info(key) - Logger.info(Kernel.inspect(value, limit: :infinity, printable_limit: :infinity)) - value - end end