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. 51
      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
### 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

@ -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)

@ -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

@ -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

@ -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

@ -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 ->

@ -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 ->

@ -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

@ -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 [
%{

@ -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 [
%{

Loading…
Cancel
Save