From 5be42bb43845932abf8234fb3e45f39cb2768d9a Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 2 Jul 2020 12:49:31 +0300 Subject: [PATCH] EIP-1967 support --- CHANGELOG.md | 1 + .../controllers/smart_contract_controller.ex | 20 +++--- .../smart_contract_controller_test.exs | 61 ++++++++++++++++++- .../lib/ethereum_jsonrpc/contract.ex | 24 +++++++- apps/explorer/lib/explorer/chain.ex | 51 ++++++++++++---- .../lib/explorer/smart_contract/reader.ex | 35 ++++++++--- .../lib/explorer/smart_contract/writer.ex | 9 +-- apps/explorer/test/explorer/chain_test.exs | 28 +++++++++ .../explorer/smart_contract/reader_test.exs | 21 ++----- .../explorer/smart_contract/writer_test.exs | 17 +----- 10 files changed, 197 insertions(+), 70 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a643888f17..5e7066f606 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## Current ### Features +- [#3174](https://github.com/poanetwork/blockscout/pull/3174) - EIP-1967 support: transparent proxy pattern - [#3173](https://github.com/poanetwork/blockscout/pull/3173) - Display implementation address at read/write proxy tabs - [#3171](https://github.com/poanetwork/blockscout/pull/3171) - Import accounts/contracts/balances from Geth genesis.json - [#3161](https://github.com/poanetwork/blockscout/pull/3161) - Write proxy contracts feature 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 038a15d4de..142005e8b6 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 @@ -14,16 +14,24 @@ defmodule BlockScoutWeb.SmartContractController do with true <- ajax?(conn), {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), {:ok, address} <- Chain.find_contract_address(address_hash, address_options, true) do + implementation_address_hash_string = + if contract_type == "proxy" do + Chain.get_implementation_address_hash(address.hash, address.smart_contract.abi) || + "0x0000000000000000000000000000000000000000" + else + "0x0000000000000000000000000000000000000000" + end + functions = if action == "write" do if contract_type == "proxy" do - Writer.write_functions_proxy(address_hash) + Writer.write_functions_proxy(implementation_address_hash_string) else Writer.write_functions(address_hash) end else if contract_type == "proxy" do - Reader.read_only_functions_proxy(address_hash) + Reader.read_only_functions_proxy(address_hash, implementation_address_hash_string) else Reader.read_only_functions(address_hash) end @@ -33,17 +41,13 @@ defmodule BlockScoutWeb.SmartContractController do implementation_abi = if contract_type == "proxy" do - address.hash - |> Chain.get_implementation_abi_from_proxy(address.smart_contract.abi) + implementation_address_hash_string + |> Chain.get_implementation_abi() |> Poison.encode!() else [] end - implementation_address_hash_string = - Chain.get_implementation_address_hash(address.hash, address.smart_contract.abi) || - "0x0000000000000000000000000000000000000000" - conn |> put_status(200) |> put_layout(false) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/smart_contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/smart_contract_controller_test.exs index a159460215..cd4e7e63bb 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/smart_contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/smart_contract_controller_test.exs @@ -68,7 +68,56 @@ defmodule BlockScoutWeb.SmartContractControllerTest do test "lists [] proxy read only functions if no verified implementation" do token_contract_address = insert(:contract_address) - insert(:smart_contract, address_hash: token_contract_address.hash) + insert(:smart_contract, + address_hash: token_contract_address.hash, + abi: [ + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [%{"type" => "address", "name" => ""}], + "name" => "implementation", + "inputs" => [], + "constant" => true + } + ] + ) + + path = + smart_contract_path(BlockScoutWeb.Endpoint, :index, + hash: token_contract_address.hash, + type: :proxy, + action: :read + ) + + conn = + build_conn() + |> put_req_header("x-requested-with", "xmlhttprequest") + |> get(path) + + assert conn.status == 200 + assert conn.assigns.read_only_functions == [] + end + + test "lists [] proxy read only functions if no verified eip-1967 implementation" do + token_contract_address = insert(:contract_address) + + insert(:smart_contract, + address_hash: token_contract_address.hash, + abi: [ + %{ + "type" => "function", + "stateMutability" => "nonpayable", + "payable" => false, + "outputs" => [%{"type" => "address", "name" => "", "internalType" => "address"}], + "name" => "implementation", + "inputs" => [], + "constant" => false + } + ] + ) + + blockchain_get_implementation_mock() path = smart_contract_path(BlockScoutWeb.Endpoint, :index, @@ -182,4 +231,14 @@ defmodule BlockScoutWeb.SmartContractControllerTest do end ) end + + defp blockchain_get_implementation_mock do + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn %{id: _, method: _, params: [_, _, _]}, _options -> + {:ok, "0xcebb2CCCFe291F0c442841cBE9C1D06EED61Ca02"} + end + ) + end end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/contract.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/contract.ex index e1c60f7352..ed0e6d614f 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/contract.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/contract.ex @@ -41,7 +41,7 @@ defmodule EthereumJSONRPC.Contract do |> Enum.map(fn {%{contract_address: contract_address, function_name: function_name, args: args} = request, index} -> functions[function_name] |> Encoder.encode_function_call(args) - |> eth_call_request(contract_address, index, Map.get(request, :block_number)) + |> eth_call_request(contract_address, index, Map.get(request, :block_number), Map.get(request, :from)) end) |> json_rpc(json_rpc_named_arguments) |> case do @@ -70,7 +70,7 @@ defmodule EthereumJSONRPC.Contract do Enum.map(requests, fn _ -> format_error(error) end) end - defp eth_call_request(data, contract_address, id, block_number) do + defp eth_call_request(data, contract_address, id, block_number, from) do block = case block_number do nil -> "latest" @@ -80,10 +80,28 @@ defmodule EthereumJSONRPC.Contract do request(%{ id: id, method: "eth_call", - params: [%{to: contract_address, data: data}, block] + params: [%{to: contract_address, data: data, from: from}, block] }) end + def eth_get_storage_at_request(contract_address, storage_pointer, block_number, json_rpc_named_arguments) do + block = + case block_number do + nil -> "latest" + block_number -> integer_to_quantity(block_number) + end + + result = + %{id: 0, method: "eth_getStorageAt", params: [contract_address, storage_pointer, block]} + |> request() + |> json_rpc(json_rpc_named_arguments) + + case result do + {:ok, storage_value} -> {:ok, storage_value} + other -> other + end + end + defp format_error(message) when is_binary(message) do {:error, message} end diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index e17cbc4f75..c9b6c6d896 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -28,6 +28,7 @@ defmodule Explorer.Chain do alias Ecto.Adapters.SQL alias Ecto.{Changeset, Multi} + alias EthereumJSONRPC.Contract alias EthereumJSONRPC.Transaction, as: EthereumJSONRPCTransaction alias Explorer.Counters.LastFetchedCounter @@ -4364,18 +4365,44 @@ defmodule Explorer.Chain do def get_implementation_address_hash(proxy_address_hash, abi) when not is_nil(proxy_address_hash) and not is_nil(abi) do - implementation_address = - case Reader.query_contract(proxy_address_hash, abi, %{ - "implementation" => [] - }) do - %{"implementation" => {:ok, [result]}} -> result - _ -> nil - end + implementation_method_abi = + abi + |> Enum.find(fn method -> + Map.get(method, "name") == "implementation" + end) + + implementation_method_abi_state_mutability = 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) - if implementation_address do - "0x" <> Base.encode16(implementation_address, case: :lower) + # 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 + ) + + implementation_address else - nil + implementation_address = + case Reader.query_contract(proxy_address_hash, abi, %{ + "implementation" => [] + }) do + %{"implementation" => {:ok, [result]}} -> result + _ -> nil + end + + if implementation_address do + "0x" <> Base.encode16(implementation_address, case: :lower) + else + nil + end end end @@ -4383,7 +4410,7 @@ defmodule Explorer.Chain do nil end - defp get_implementation_abi(implementation_address_hash_string) when not is_nil(implementation_address_hash_string) do + 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} -> implementation_smart_contract = @@ -4402,7 +4429,7 @@ defmodule Explorer.Chain do end end - defp get_implementation_abi(implementation_address_hash_string) when is_nil(implementation_address_hash_string) do + def get_implementation_abi(implementation_address_hash_string) when is_nil(implementation_address_hash_string) do [] end diff --git a/apps/explorer/lib/explorer/smart_contract/reader.ex b/apps/explorer/lib/explorer/smart_contract/reader.ex index 933922ecc3..72eef9aa95 100644 --- a/apps/explorer/lib/explorer/smart_contract/reader.ex +++ b/apps/explorer/lib/explorer/smart_contract/reader.ex @@ -112,6 +112,32 @@ defmodule Explorer.SmartContract.Reader do end) end + @spec query_contract( + String.t(), + String.t(), + term(), + functions() + ) :: functions_results() + def query_contract(contract_address, from, abi, functions) do + requests = + functions + |> Enum.map(fn {function_name, args} -> + %{ + contract_address: contract_address, + from: from, + function_name: function_name, + args: args + } + end) + + requests + |> query_contracts(abi) + |> Enum.zip(requests) + |> Enum.into(%{}, fn {response, request} -> + {request.function_name, response} + end) + end + @doc """ Runs batch of contract functions on given addresses for smart contract with an expected ABI and functions. @@ -180,13 +206,8 @@ defmodule Explorer.SmartContract.Reader do end end - def read_only_functions_proxy(contract_address_hash) do - abi = - contract_address_hash - |> Chain.address_hash_to_smart_contract() - |> Map.get(:abi) - - implementation_abi = Chain.get_implementation_abi_from_proxy(contract_address_hash, abi) + def read_only_functions_proxy(contract_address_hash, implementation_address_hash_string) do + implementation_abi = Chain.get_implementation_abi(implementation_address_hash_string) case implementation_abi do nil -> diff --git a/apps/explorer/lib/explorer/smart_contract/writer.ex b/apps/explorer/lib/explorer/smart_contract/writer.ex index 3193cbc657..872742fe73 100644 --- a/apps/explorer/lib/explorer/smart_contract/writer.ex +++ b/apps/explorer/lib/explorer/smart_contract/writer.ex @@ -23,13 +23,8 @@ defmodule Explorer.SmartContract.Writer do end @spec write_functions_proxy(Hash.t()) :: [%{}] - def write_functions_proxy(contract_address_hash) do - abi = - contract_address_hash - |> Chain.address_hash_to_smart_contract() - |> Map.get(:abi) - - implementation_abi = Chain.get_implementation_abi_from_proxy(contract_address_hash, abi) + def write_functions_proxy(implementation_address_hash_string) do + implementation_abi = Chain.get_implementation_abi(implementation_address_hash_string) case implementation_abi do nil -> diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index 639dc51688..dc9b9f59ba 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -5410,5 +5410,33 @@ defmodule Explorer.ChainTest do assert implementation_abi == @implementation_abi end + + test "get_implementation_abi/1 returns empty [] abi if implmentation address is null" do + assert Chain.get_implementation_abi(nil) == [] + end + + test "get_implementation_abi/1 returns [] if implementation is not verified" do + implementation_contract_address = insert(:contract_address) + + implementation_contract_address_hash_string = + Base.encode16(implementation_contract_address.hash.bytes, case: :lower) + + assert Chain.get_implementation_abi("0x" <> implementation_contract_address_hash_string) == [] + end + + test "get_implementation_abi/1 returns 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) + + implementation_abi = Chain.get_implementation_abi("0x" <> implementation_contract_address_hash_string) + + assert implementation_abi == @implementation_abi + end end end diff --git a/apps/explorer/test/explorer/smart_contract/reader_test.exs b/apps/explorer/test/explorer/smart_contract/reader_test.exs index 9b3e14f8c2..17ede61237 100644 --- a/apps/explorer/test/explorer/smart_contract/reader_test.exs +++ b/apps/explorer/test/explorer/smart_contract/reader_test.exs @@ -220,24 +220,13 @@ defmodule Explorer.SmartContract.ReaderTest do 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 - ) - blockchain_get_function_mock() - response = Reader.read_only_functions_proxy(proxy_smart_contract.address_hash) + response = + Reader.read_only_functions_proxy( + proxy_smart_contract.address_hash, + "0x" <> implementation_contract_address_hash_string + ) assert [ %{ diff --git a/apps/explorer/test/explorer/smart_contract/writer_test.exs b/apps/explorer/test/explorer/smart_contract/writer_test.exs index 91b239f8db..00037b7149 100644 --- a/apps/explorer/test/explorer/smart_contract/writer_test.exs +++ b/apps/explorer/test/explorer/smart_contract/writer_test.exs @@ -296,22 +296,7 @@ defmodule Explorer.SmartContract.WriterTest do 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) + response = Writer.write_functions_proxy("0x" <> implementation_contract_address_hash_string) assert [ %{