Merge pull request #3174 from poanetwork/vb-eip1967-transparent-proxy-support

EIP-1967 support: transparent proxy pattern
pull/3175/head
Victor Baranov 4 years ago committed by GitHub
commit 672dfd2cb8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 20
      apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex
  3. 61
      apps/block_scout_web/test/block_scout_web/controllers/smart_contract_controller_test.exs
  4. 24
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/contract.ex
  5. 31
      apps/explorer/lib/explorer/chain.ex
  6. 35
      apps/explorer/lib/explorer/smart_contract/reader.ex
  7. 9
      apps/explorer/lib/explorer/smart_contract/writer.ex
  8. 28
      apps/explorer/test/explorer/chain_test.exs
  9. 21
      apps/explorer/test/explorer/smart_contract/reader_test.exs
  10. 17
      apps/explorer/test/explorer/smart_contract/writer_test.exs

@ -1,6 +1,7 @@
## Current ## Current
### Features ### 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 - [#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 - [#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 - [#3161](https://github.com/poanetwork/blockscout/pull/3161) - Write proxy contracts feature

@ -14,16 +14,24 @@ defmodule BlockScoutWeb.SmartContractController do
with true <- ajax?(conn), with true <- ajax?(conn),
{:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, address} <- Chain.find_contract_address(address_hash, address_options, true) do {: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 = functions =
if action == "write" do if action == "write" do
if contract_type == "proxy" do if contract_type == "proxy" do
Writer.write_functions_proxy(address_hash) Writer.write_functions_proxy(implementation_address_hash_string)
else else
Writer.write_functions(address_hash) Writer.write_functions(address_hash)
end end
else else
if contract_type == "proxy" do if contract_type == "proxy" do
Reader.read_only_functions_proxy(address_hash) Reader.read_only_functions_proxy(address_hash, implementation_address_hash_string)
else else
Reader.read_only_functions(address_hash) Reader.read_only_functions(address_hash)
end end
@ -33,17 +41,13 @@ defmodule BlockScoutWeb.SmartContractController do
implementation_abi = implementation_abi =
if contract_type == "proxy" do if contract_type == "proxy" do
address.hash implementation_address_hash_string
|> Chain.get_implementation_abi_from_proxy(address.smart_contract.abi) |> Chain.get_implementation_abi()
|> Poison.encode!() |> Poison.encode!()
else else
[] []
end end
implementation_address_hash_string =
Chain.get_implementation_address_hash(address.hash, address.smart_contract.abi) ||
"0x0000000000000000000000000000000000000000"
conn conn
|> put_status(200) |> put_status(200)
|> put_layout(false) |> put_layout(false)

@ -68,7 +68,56 @@ defmodule BlockScoutWeb.SmartContractControllerTest do
test "lists [] proxy read only functions if no verified implementation" do test "lists [] proxy read only functions if no verified implementation" do
token_contract_address = insert(:contract_address) 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 = path =
smart_contract_path(BlockScoutWeb.Endpoint, :index, smart_contract_path(BlockScoutWeb.Endpoint, :index,
@ -182,4 +231,14 @@ defmodule BlockScoutWeb.SmartContractControllerTest do
end end
) )
end end
defp blockchain_get_implementation_mock do
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn %{id: _, method: _, params: [_, _, _]}, _options ->
{:ok, "0xcebb2CCCFe291F0c442841cBE9C1D06EED61Ca02"}
end
)
end
end end

@ -41,7 +41,7 @@ defmodule EthereumJSONRPC.Contract do
|> Enum.map(fn {%{contract_address: contract_address, function_name: function_name, args: args} = request, index} -> |> Enum.map(fn {%{contract_address: contract_address, function_name: function_name, args: args} = request, index} ->
functions[function_name] functions[function_name]
|> Encoder.encode_function_call(args) |> 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) end)
|> json_rpc(json_rpc_named_arguments) |> json_rpc(json_rpc_named_arguments)
|> case do |> case do
@ -70,7 +70,7 @@ defmodule EthereumJSONRPC.Contract do
Enum.map(requests, fn _ -> format_error(error) end) Enum.map(requests, fn _ -> format_error(error) end)
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 = block =
case block_number do case block_number do
nil -> "latest" nil -> "latest"
@ -80,10 +80,28 @@ defmodule EthereumJSONRPC.Contract do
request(%{ request(%{
id: id, id: id,
method: "eth_call", method: "eth_call",
params: [%{to: contract_address, data: data}, block] params: [%{to: contract_address, data: data, from: from}, block]
}) })
end 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 defp format_error(message) when is_binary(message) do
{:error, message} {:error, message}
end end

@ -28,6 +28,7 @@ defmodule Explorer.Chain do
alias Ecto.Adapters.SQL alias Ecto.Adapters.SQL
alias Ecto.{Changeset, Multi} alias Ecto.{Changeset, Multi}
alias EthereumJSONRPC.Contract
alias EthereumJSONRPC.Transaction, as: EthereumJSONRPCTransaction alias EthereumJSONRPC.Transaction, as: EthereumJSONRPCTransaction
alias Explorer.Counters.LastFetchedCounter alias Explorer.Counters.LastFetchedCounter
@ -4364,6 +4365,31 @@ defmodule Explorer.Chain do
def get_implementation_address_hash(proxy_address_hash, abi) def get_implementation_address_hash(proxy_address_hash, abi)
when not is_nil(proxy_address_hash) and not is_nil(abi) do when not is_nil(proxy_address_hash) and not is_nil(abi) do
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)
# 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
implementation_address = implementation_address =
case Reader.query_contract(proxy_address_hash, abi, %{ case Reader.query_contract(proxy_address_hash, abi, %{
"implementation" => [] "implementation" => []
@ -4378,12 +4404,13 @@ defmodule Explorer.Chain do
nil nil
end end
end end
end
def get_implementation_address_hash(proxy_address_hash, abi) when is_nil(proxy_address_hash) or is_nil(abi) do def get_implementation_address_hash(proxy_address_hash, abi) when is_nil(proxy_address_hash) or is_nil(abi) do
nil nil
end 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 case Chain.string_to_address_hash(implementation_address_hash_string) do
{:ok, implementation_address_hash} -> {:ok, implementation_address_hash} ->
implementation_smart_contract = implementation_smart_contract =
@ -4402,7 +4429,7 @@ defmodule Explorer.Chain do
end end
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 end

@ -112,6 +112,32 @@ defmodule Explorer.SmartContract.Reader do
end) end)
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 """ @doc """
Runs batch of contract functions on given addresses for smart contract with an expected ABI and functions. 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
end end
def read_only_functions_proxy(contract_address_hash) do def read_only_functions_proxy(contract_address_hash, implementation_address_hash_string) do
abi = implementation_abi = Chain.get_implementation_abi(implementation_address_hash_string)
contract_address_hash
|> Chain.address_hash_to_smart_contract()
|> Map.get(:abi)
implementation_abi = Chain.get_implementation_abi_from_proxy(contract_address_hash, abi)
case implementation_abi do case implementation_abi do
nil -> nil ->

@ -23,13 +23,8 @@ defmodule Explorer.SmartContract.Writer do
end end
@spec write_functions_proxy(Hash.t()) :: [%{}] @spec write_functions_proxy(Hash.t()) :: [%{}]
def write_functions_proxy(contract_address_hash) do def write_functions_proxy(implementation_address_hash_string) do
abi = implementation_abi = Chain.get_implementation_abi(implementation_address_hash_string)
contract_address_hash
|> Chain.address_hash_to_smart_contract()
|> Map.get(:abi)
implementation_abi = Chain.get_implementation_abi_from_proxy(contract_address_hash, abi)
case implementation_abi do case implementation_abi do
nil -> nil ->

@ -5410,5 +5410,33 @@ defmodule Explorer.ChainTest do
assert implementation_abi == @implementation_abi assert implementation_abi == @implementation_abi
end 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
end end

@ -220,24 +220,13 @@ defmodule Explorer.SmartContract.ReaderTest do
implementation_contract_address_hash_string = implementation_contract_address_hash_string =
Base.encode16(implementation_contract_address.hash.bytes, case: :lower) 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() 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 [ assert [
%{ %{

@ -296,22 +296,7 @@ defmodule Explorer.SmartContract.WriterTest do
implementation_contract_address_hash_string = implementation_contract_address_hash_string =
Base.encode16(implementation_contract_address.hash.bytes, case: :lower) Base.encode16(implementation_contract_address.hash.bytes, case: :lower)
expect( response = Writer.write_functions_proxy("0x" <> implementation_contract_address_hash_string)
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 [ assert [
%{ %{

Loading…
Cancel
Save