From 3877fb170dcfd878171aeb93221b85bddd6fce7c Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 30 Jun 2020 12:26:04 +0300 Subject: [PATCH] Add tests --- .../views/smart_contract_view.ex | 21 +- ...address_write_contract_controller_test.exs | 81 +++++ .../address_write_proxy_controller_test.exs | 81 +++++ .../address_write_contract_view_test.exs | 19 + .../address_write_proxy_view_test copy.exs | 19 + .../views/tokens/smart_contract_view_test.exs | 177 +++++++++ .../explorer/smart_contract/writer_test.exs | 344 ++++++++++++++++++ 7 files changed, 738 insertions(+), 4 deletions(-) create mode 100644 apps/block_scout_web/test/block_scout_web/controllers/address_write_contract_controller_test.exs create mode 100644 apps/block_scout_web/test/block_scout_web/controllers/address_write_proxy_controller_test.exs create mode 100644 apps/block_scout_web/test/block_scout_web/views/address_write_contract_view_test.exs create mode 100644 apps/block_scout_web/test/block_scout_web/views/address_write_proxy_view_test copy.exs create mode 100644 apps/explorer/test/explorer/smart_contract/writer_test.exs 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 1cde5f4df3..f0449424fb 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 @@ -5,18 +5,31 @@ defmodule BlockScoutWeb.SmartContractView do def queryable?(inputs) when is_nil(inputs), do: false - def writeable?(function), - do: payable?(function) || nonpayable?(function) || fallback?(function) || function["constant"] == false + def writeable?(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 outputs?(outputs) when not is_nil(outputs), do: Enum.any?(outputs) def outputs?(outputs) when is_nil(outputs), do: false - def fallback?(function), do: function["type"] == "fallback" + defp event?(function), do: function["type"] == "event" + + defp constructor?(function), do: function["type"] == "constructor" def payable?(function), do: function["stateMutability"] == "payable" || function["payable"] - def nonpayable?(function), do: function["stateMutability"] == "nonpayable" + def nonpayable?(function) do + if function["type"] do + function["stateMutability"] == "nonpayable" || + (!function["payable"] && !function["constant"] && !function["stateMutability"]) + else + false + end + end def address?(type), do: type in ["address", "address payable"] diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_write_contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_write_contract_controller_test.exs new file mode 100644 index 0000000000..9356c474ea --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/address_write_contract_controller_test.exs @@ -0,0 +1,81 @@ +defmodule BlockScoutWeb.AddressWriteContractControllerTest do + use BlockScoutWeb.ConnCase, async: true + + alias Explorer.ExchangeRates.Token + alias Explorer.Chain.Address + + import Mox + + describe "GET index/3" do + setup :set_mox_global + + setup do + configuration = Application.get_env(:explorer, :checksum_function) + Application.put_env(:explorer, :checksum_function, :eth) + + :ok + + on_exit(fn -> + Application.put_env(:explorer, :checksum_function, configuration) + end) + end + + test "with invalid address hash", %{conn: conn} do + conn = get(conn, address_write_contract_path(BlockScoutWeb.Endpoint, :index, "invalid_address")) + + assert html_response(conn, 404) + end + + test "with valid address that is not a contract", %{conn: conn} do + address = insert(:address) + + conn = get(conn, address_write_contract_path(BlockScoutWeb.Endpoint, :index, Address.checksum(address.hash))) + + assert html_response(conn, 404) + end + + test "successfully renders the page when the address is a contract", %{conn: conn} do + contract_address = insert(:contract_address) + + transaction = insert(:transaction, from_address: contract_address) |> with_block() + + insert( + :internal_transaction_create, + index: 0, + transaction: transaction, + created_contract_address: contract_address, + block_hash: transaction.block_hash, + block_index: 0 + ) + + insert(:smart_contract, address_hash: contract_address.hash) + + conn = + get(conn, address_write_contract_path(BlockScoutWeb.Endpoint, :index, Address.checksum(contract_address.hash))) + + assert html_response(conn, 200) + assert contract_address.hash == conn.assigns.address.hash + assert %Token{} = conn.assigns.exchange_rate + end + + test "returns not found for an unverified contract", %{conn: conn} do + contract_address = insert(:contract_address) + + transaction = insert(:transaction, from_address: contract_address) |> with_block() + + insert( + :internal_transaction_create, + index: 0, + transaction: transaction, + created_contract_address: contract_address, + block_hash: transaction.block_hash, + block_index: 0 + ) + + conn = + get(conn, address_write_contract_path(BlockScoutWeb.Endpoint, :index, Address.checksum(contract_address.hash))) + + assert html_response(conn, 404) + end + end +end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_write_proxy_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_write_proxy_controller_test.exs new file mode 100644 index 0000000000..d4fc4917aa --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/address_write_proxy_controller_test.exs @@ -0,0 +1,81 @@ +defmodule BlockScoutWeb.AddressWriteProxyControllerTest do + use BlockScoutWeb.ConnCase, async: true + + alias Explorer.ExchangeRates.Token + alias Explorer.Chain.Address + + import Mox + + describe "GET index/3" do + setup :set_mox_global + + setup do + configuration = Application.get_env(:explorer, :checksum_function) + Application.put_env(:explorer, :checksum_function, :eth) + + :ok + + on_exit(fn -> + Application.put_env(:explorer, :checksum_function, configuration) + end) + end + + test "with invalid address hash", %{conn: conn} do + conn = get(conn, address_write_proxy_path(BlockScoutWeb.Endpoint, :index, "invalid_address")) + + assert html_response(conn, 404) + end + + test "with valid address that is not a contract", %{conn: conn} do + address = insert(:address) + + conn = get(conn, address_write_proxy_path(BlockScoutWeb.Endpoint, :index, Address.checksum(address.hash))) + + assert html_response(conn, 404) + end + + test "successfully renders the page when the address is a contract", %{conn: conn} do + contract_address = insert(:contract_address) + + transaction = insert(:transaction, from_address: contract_address) |> with_block() + + insert( + :internal_transaction_create, + index: 0, + transaction: transaction, + created_contract_address: contract_address, + block_hash: transaction.block_hash, + block_index: 0 + ) + + insert(:smart_contract, address_hash: contract_address.hash) + + conn = + get(conn, address_write_proxy_path(BlockScoutWeb.Endpoint, :index, Address.checksum(contract_address.hash))) + + assert html_response(conn, 200) + assert contract_address.hash == conn.assigns.address.hash + assert %Token{} = conn.assigns.exchange_rate + end + + test "returns not found for an unverified contract", %{conn: conn} do + contract_address = insert(:contract_address) + + transaction = insert(:transaction, from_address: contract_address) |> with_block() + + insert( + :internal_transaction_create, + index: 0, + transaction: transaction, + created_contract_address: contract_address, + block_hash: transaction.block_hash, + block_index: 0 + ) + + conn = + get(conn, address_write_proxy_path(BlockScoutWeb.Endpoint, :index, Address.checksum(contract_address.hash))) + + assert html_response(conn, 404) + end + end +end diff --git a/apps/block_scout_web/test/block_scout_web/views/address_write_contract_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/address_write_contract_view_test.exs new file mode 100644 index 0000000000..55360f1229 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/views/address_write_contract_view_test.exs @@ -0,0 +1,19 @@ +defmodule BlockScoutWeb.AddressWriteContractViewTest do + use BlockScoutWeb.ConnCase, async: true + + alias BlockScoutWeb.AddressWriteContractView + + describe "queryable?/1" do + test "returns true if list of inputs is not empty" do + assert AddressWriteContractView.queryable?([%{"name" => "argument_name", "type" => "uint256"}]) == true + assert AddressWriteContractView.queryable?([]) == false + end + end + + describe "address?/1" do + test "returns true if type equals `address`" do + assert AddressWriteContractView.address?("address") == true + assert AddressWriteContractView.address?("uint256") == false + end + end +end diff --git a/apps/block_scout_web/test/block_scout_web/views/address_write_proxy_view_test copy.exs b/apps/block_scout_web/test/block_scout_web/views/address_write_proxy_view_test copy.exs new file mode 100644 index 0000000000..e3db0d7003 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/views/address_write_proxy_view_test copy.exs @@ -0,0 +1,19 @@ +defmodule BlockScoutWeb.AddressWriteProxyViewTest do + use BlockScoutWeb.ConnCase, async: true + + alias BlockScoutWeb.AddressWriteProxyView + + describe "queryable?/1" do + test "returns true if list of inputs is not empty" do + assert AddressWriteProxyView.queryable?([%{"name" => "argument_name", "type" => "uint256"}]) == true + assert AddressWriteProxyView.queryable?([]) == false + end + end + + describe "address?/1" do + test "returns true if type equals `address`" do + assert AddressWriteProxyView.address?("address") == true + assert AddressWriteProxyView.address?("uint256") == false + end + end +end 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 5ab8133bf5..cc812cd41b 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,6 +17,183 @@ defmodule BlockScoutWeb.SmartContractViewTest do end end + describe "writeable?" do + test "returns true when there is write function" do + function = %{ + "type" => "function", + "stateMutability" => "nonpayable", + "payable" => false, + "outputs" => [], + "name" => "upgradeTo", + "inputs" => [%{"type" => "uint256", "name" => "version"}, %{"type" => "address", "name" => "implementation"}], + "constant" => false + } + + assert SmartContractView.writeable?(function) + end + + test "returns false when it is not a write function" do + function = %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "uint256", "name" => ""}], + "name" => "version", + "inputs" => [], + "constant" => true + } + + refute SmartContractView.writeable?(function) + end + + test "returns false when there is no function" do + function = %{} + + refute SmartContractView.writeable?(function) + end + + test "returns false when there function is nil" do + function = nil + + refute SmartContractView.writeable?(function) + end + end + + describe "outputs?" do + test "returns true when there are outputs" do + outputs = [%{"name" => "_narcoId", "type" => "uint256"}] + + assert SmartContractView.outputs?(outputs) + end + + test "returns false when there are no outputs" do + outputs = [] + + refute SmartContractView.outputs?(outputs) + end + end + + describe "payable?" do + test "returns true when there is payable function" do + function = %{ + "type" => "function", + "stateMutability" => "payable", + "payable" => true, + "outputs" => [], + "name" => "upgradeToAndCall", + "inputs" => [ + %{"type" => "uint256", "name" => "version"}, + %{"type" => "address", "name" => "implementation"}, + %{"type" => "bytes", "name" => "data"} + ], + "constant" => false + } + + assert SmartContractView.payable?(function) + end + + test "returns true when there is old-style payable function" do + function = %{ + "type" => "function", + "payable" => true, + "outputs" => [], + "name" => "upgradeToAndCall", + "inputs" => [ + %{"type" => "uint256", "name" => "version"}, + %{"type" => "address", "name" => "implementation"}, + %{"type" => "bytes", "name" => "data"} + ], + "constant" => false + } + + assert SmartContractView.payable?(function) + end + + test "returns false when it is nonpayable function" do + function = %{ + "type" => "function", + "stateMutability" => "nonpayable", + "payable" => false, + "outputs" => [], + "name" => "transferProxyOwnership", + "inputs" => [%{"type" => "address", "name" => "newOwner"}], + "constant" => false + } + + refute SmartContractView.payable?(function) + end + + test "returns false when there is no function" do + function = %{} + + refute SmartContractView.payable?(function) + end + + test "returns false when function is nil" do + function = nil + + refute SmartContractView.payable?(function) + end + end + + describe "nonpayable?" do + test "returns true when there is nonpayable function" do + function = %{ + "type" => "function", + "stateMutability" => "nonpayable", + "payable" => false, + "outputs" => [], + "name" => "transferProxyOwnership", + "inputs" => [%{"type" => "address", "name" => "newOwner"}], + "constant" => false + } + + assert SmartContractView.nonpayable?(function) + end + + test "returns true when there is old-style nonpayable function" do + function = %{ + "type" => "function", + "outputs" => [], + "name" => "test", + "inputs" => [%{"type" => "address", "name" => "newOwner"}], + "constant" => false + } + + assert SmartContractView.nonpayable?(function) + end + + test "returns false when it is payable function" do + function = %{ + "type" => "function", + "stateMutability" => "payable", + "payable" => true, + "outputs" => [], + "name" => "upgradeToAndCall", + "inputs" => [ + %{"type" => "uint256", "name" => "version"}, + %{"type" => "address", "name" => "implementation"}, + %{"type" => "bytes", "name" => "data"} + ], + "constant" => false + } + + refute SmartContractView.nonpayable?(function) + end + + test "returns true when there is no function" do + function = %{} + + refute SmartContractView.nonpayable?(function) + end + + test "returns false when function is nil" do + function = nil + + refute SmartContractView.nonpayable?(function) + end + end + describe "address?" do test "returns true when the type is equal to the string 'address'" do type = "address" diff --git a/apps/explorer/test/explorer/smart_contract/writer_test.exs b/apps/explorer/test/explorer/smart_contract/writer_test.exs new file mode 100644 index 0000000000..91b239f8db --- /dev/null +++ b/apps/explorer/test/explorer/smart_contract/writer_test.exs @@ -0,0 +1,344 @@ +defmodule Explorer.SmartContract.WriterTest do + use EthereumJSONRPC.Case + use Explorer.DataCase + + import Mox + + alias Explorer.SmartContract.Writer + + @abi [ + %{ + "type" => "function", + "stateMutability" => "nonpayable", + "payable" => false, + "outputs" => [], + "name" => "upgradeTo", + "inputs" => [%{"type" => "uint256", "name" => "version"}, %{"type" => "address", "name" => "implementation"}], + "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" => "view", + "payable" => false, + "outputs" => [%{"type" => "address", "name" => ""}], + "name" => "upgradeabilityOwner", + "inputs" => [], + "constant" => true + }, + %{ + "type" => "function", + "stateMutability" => "payable", + "payable" => true, + "outputs" => [], + "name" => "upgradeToAndCall", + "inputs" => [ + %{"type" => "uint256", "name" => "version"}, + %{"type" => "address", "name" => "implementation"}, + %{"type" => "bytes", "name" => "data"} + ], + "constant" => false + }, + %{ + "type" => "function", + "stateMutability" => "nonpayable", + "payable" => false, + "outputs" => [], + "name" => "transferProxyOwnership", + "inputs" => [%{"type" => "address", "name" => "newOwner"}], + "constant" => false + }, + %{"type" => "fallback", "stateMutability" => "payable", "payable" => true}, + %{ + "type" => "event", + "name" => "ProxyOwnershipTransferred", + "inputs" => [ + %{"type" => "address", "name" => "previousOwner", "indexed" => false}, + %{"type" => "address", "name" => "newOwner", "indexed" => false} + ], + "anonymous" => false + }, + %{ + "type" => "event", + "name" => "Upgraded", + "inputs" => [ + %{"type" => "uint256", "name" => "version", "indexed" => false}, + %{"type" => "address", "name" => "implementation", "indexed" => true} + ], + "anonymous" => false + } + ] + + @implementation_abi [ + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "uint256", "name" => ""}], + "name" => "extraReceiverAmount", + "inputs" => [%{"type" => "address", "name" => "_receiver"}], + "constant" => true + }, + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "uint256", "name" => ""}], + "name" => "bridgesAllowedLength", + "inputs" => [], + "constant" => true + }, + %{ + "type" => "function", + "stateMutability" => "pure", + "payable" => false, + "outputs" => [%{"type" => "bytes4", "name" => ""}], + "name" => "blockRewardContractId", + "inputs" => [], + "constant" => true + }, + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "uint256", "name" => ""}], + "name" => "mintedForAccountInBlock", + "inputs" => [%{"type" => "address", "name" => "_account"}, %{"type" => "uint256", "name" => "_blockNumber"}], + "constant" => true + }, + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "uint256", "name" => ""}], + "name" => "mintedForAccount", + "inputs" => [%{"type" => "address", "name" => "_account"}], + "constant" => true + }, + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "uint256", "name" => ""}], + "name" => "mintedInBlock", + "inputs" => [%{"type" => "uint256", "name" => "_blockNumber"}], + "constant" => true + }, + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "uint256", "name" => ""}], + "name" => "mintedTotally", + "inputs" => [], + "constant" => true + }, + %{ + "type" => "function", + "stateMutability" => "pure", + "payable" => false, + "outputs" => [%{"type" => "address[1]", "name" => ""}], + "name" => "bridgesAllowed", + "inputs" => [], + "constant" => true + }, + %{ + "type" => "function", + "stateMutability" => "nonpayable", + "payable" => false, + "outputs" => [], + "name" => "addExtraReceiver", + "inputs" => [%{"type" => "uint256", "name" => "_amount"}, %{"type" => "address", "name" => "_receiver"}], + "constant" => false + }, + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "uint256", "name" => ""}], + "name" => "mintedTotallyByBridge", + "inputs" => [%{"type" => "address", "name" => "_bridge"}], + "constant" => true + }, + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "address", "name" => ""}], + "name" => "extraReceiverByIndex", + "inputs" => [%{"type" => "uint256", "name" => "_index"}], + "constant" => true + }, + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "uint256", "name" => ""}], + "name" => "bridgeAmount", + "inputs" => [%{"type" => "address", "name" => "_bridge"}], + "constant" => true + }, + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "uint256", "name" => ""}], + "name" => "extraReceiversLength", + "inputs" => [], + "constant" => true + }, + %{ + "type" => "function", + "stateMutability" => "nonpayable", + "payable" => false, + "outputs" => [%{"type" => "address[]", "name" => ""}, %{"type" => "uint256[]", "name" => ""}], + "name" => "reward", + "inputs" => [%{"type" => "address[]", "name" => "benefactors"}, %{"type" => "uint16[]", "name" => "kind"}], + "constant" => false + }, + %{ + "type" => "event", + "name" => "AddedReceiver", + "inputs" => [ + %{"type" => "uint256", "name" => "amount", "indexed" => false}, + %{"type" => "address", "name" => "receiver", "indexed" => true}, + %{"type" => "address", "name" => "bridge", "indexed" => true} + ], + "anonymous" => false + } + ] + + doctest Explorer.SmartContract.Writer + + setup :verify_on_exit! + + describe "write_functions/1" do + test "fetches the smart contract write functions" do + smart_contract = + insert( + :smart_contract, + abi: @abi + ) + + response = Writer.write_functions(smart_contract.address_hash) + + assert [ + %{ + "type" => "function", + "stateMutability" => "nonpayable", + "payable" => false, + "outputs" => [], + "name" => "upgradeTo", + "inputs" => [ + %{"type" => "uint256", "name" => "version"}, + %{"type" => "address", "name" => "implementation"} + ], + "constant" => false + }, + %{ + "type" => "function", + "stateMutability" => "payable", + "payable" => true, + "outputs" => [], + "name" => "upgradeToAndCall", + "inputs" => [ + %{"type" => "uint256", "name" => "version"}, + %{"type" => "address", "name" => "implementation"}, + %{"type" => "bytes", "name" => "data"} + ], + "constant" => false + }, + %{ + "type" => "function", + "stateMutability" => "nonpayable", + "payable" => false, + "outputs" => [], + "name" => "transferProxyOwnership", + "inputs" => [%{"type" => "address", "name" => "newOwner"}], + "constant" => false + }, + %{"type" => "fallback", "stateMutability" => "payable", "payable" => true} + ] = response + end + end + + describe "write_functions_proxy/1" do + test "fetches the smart contract proxy write functions" do + proxy_smart_contract = + insert(:smart_contract, + abi: @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 + ) + + response = Writer.write_functions_proxy(proxy_smart_contract.address_hash) + + assert [ + %{ + "type" => "function", + "stateMutability" => "nonpayable", + "payable" => false, + "outputs" => [], + "name" => "addExtraReceiver", + "inputs" => [ + %{"type" => "uint256", "name" => "_amount"}, + %{"type" => "address", "name" => "_receiver"} + ], + "constant" => false + }, + %{ + "type" => "function", + "stateMutability" => "nonpayable", + "payable" => false, + "outputs" => [%{"type" => "address[]", "name" => ""}, %{"type" => "uint256[]", "name" => ""}], + "name" => "reward", + "inputs" => [ + %{"type" => "address[]", "name" => "benefactors"}, + %{"type" => "uint16[]", "name" => "kind"} + ], + "constant" => false + } + ] = response + end + end +end