parent
0692487cb5
commit
db4bf40cca
@ -0,0 +1,47 @@ |
||||
defmodule BlockScoutWeb.AddressContractVerificationVyperController do |
||||
use BlockScoutWeb, :controller |
||||
|
||||
alias Explorer.Chain.SmartContract |
||||
alias Explorer.SmartContract.{CompilerVersion, Vyper.PublisherWorker} |
||||
|
||||
def new(conn, %{"address_id" => address_hash_string}) do |
||||
changeset = |
||||
SmartContract.changeset( |
||||
%SmartContract{address_hash: address_hash_string}, |
||||
%{} |
||||
) |
||||
|
||||
compiler_versions = |
||||
case CompilerVersion.fetch_versions(:vyper) do |
||||
{:ok, compiler_versions} -> |
||||
compiler_versions |
||||
|
||||
{:error, _} -> |
||||
[] |
||||
end |
||||
|
||||
render(conn, "new.html", |
||||
changeset: changeset, |
||||
compiler_versions: compiler_versions, |
||||
address_hash: address_hash_string |
||||
) |
||||
end |
||||
|
||||
def create( |
||||
conn, |
||||
%{ |
||||
"smart_contract" => smart_contract |
||||
} |
||||
) do |
||||
Que.add(PublisherWorker, {smart_contract["address_hash"], smart_contract, conn}) |
||||
|
||||
send_resp(conn, 204, "") |
||||
end |
||||
|
||||
def parse_optimization_runs(%{"runs" => runs}) do |
||||
case Integer.parse(runs) do |
||||
{integer, ""} -> integer |
||||
_ -> 200 |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,102 @@ |
||||
<% metadata_for_verification = Chain.get_address_verified_twin_contract(@address_hash).verified_contract %> |
||||
<% contract_name_value = if metadata_for_verification, do: metadata_for_verification.name, else: "Vyper_contract" %> |
||||
<% compiler_version = if metadata_for_verification, do: metadata_for_verification.compiler_version, else: "latest" %> |
||||
<% contract_source_code_value = if metadata_for_verification, do: metadata_for_verification.contract_source_code, else: "" %> |
||||
<section data-page="contract-verification" class="container new-smart-contract-container"> |
||||
<div data-selector="channel-disconnected-message" class="d-none"> |
||||
<div data-selector="reload-button" class="alert alert-danger"> |
||||
<a href="#" class="alert-link"><%= gettext "Connection Lost" %></a> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="new-smart-contract-form"> |
||||
<h1 class="smart-contract-title"><%= gettext "New Vyper Smart Contract Verification" %></h1> |
||||
|
||||
<%= form_for @changeset, |
||||
address_contract_verification_path(@conn, :create), |
||||
[], |
||||
fn f -> %> |
||||
|
||||
<div class="smart-contract-form-group"> |
||||
<div class="smart-contract-form-group-inner-wrapper"> |
||||
<%= label f, :address_hash, gettext("Contract Address") %> |
||||
<div class="center-column"> |
||||
<%= text_input f, :address_hash, class: "form-control border-rounded", "aria-describedby": "contract-address-help-block", readonly: true %> |
||||
<%= error_tag f, :address_hash, id: "contract-address-help-block", class: "text-danger form-error" %> |
||||
</div> |
||||
<div class="smart-contract-form-group-tooltip">The 0x address supplied on contract creation.</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="smart-contract-form-group"> |
||||
<div class="smart-contract-form-group-inner-wrapper"> |
||||
<%= label f, :name, gettext("Contract Name") %> |
||||
<div class="center-column"> |
||||
<%= text_input f, :name, class: "form-control border-rounded", "aria-describedby": "contract-name-help-block", "data-test": "contract_name", value: contract_name_value, disabled: "true" %> |
||||
<%= error_tag f, :name, id: "contract-name-help-block", class: "text-danger form-error" %> |
||||
</div> |
||||
<div class="smart-contract-form-group-tooltip">Must match the name specified in the code.</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="smart-contract-form-group"> |
||||
<div class="smart-contract-form-group-inner-wrapper"> |
||||
<%= label f, :compiler_version, gettext("Compiler") %> |
||||
<div class="center-column"> |
||||
<%= select f, :compiler_version, @compiler_versions, class: "form-control border-rounded", selected: compiler_version, "aria-describedby": "compiler-help-block" %> |
||||
<%= error_tag f, :compiler_version, id: "compiler-help-block", class: "text-danger form-error" %> |
||||
</div> |
||||
<div class="smart-contract-form-group-tooltip"></div> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="smart-contract-form-group"> |
||||
<div class="smart-contract-form-group-inner-wrapper"> |
||||
<%= label f, :contract_source_code, gettext("Enter the Vyper Contract Code") %> |
||||
<div class="center-column"> |
||||
<%= textarea f, :contract_source_code, class: "form-control border-rounded monospace", rows: 3, "aria-describedby": "contract-source-code-help-block", value: contract_source_code_value %> |
||||
<%= error_tag f, :contract_source_code, id: "contract-source-code-help-block", class: "text-danger form-error", "data-test": "contract-source-code-error" %> |
||||
</div> |
||||
<div class="smart-contract-form-group-tooltip"></div> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="smart-contract-form-group constructor-arguments" style="display: block"> |
||||
<div class="smart-contract-form-group-inner-wrapper"> |
||||
<%= label f, :constructor_arguments, gettext("ABI-encoded Constructor Arguments (if required by the contract)") %> |
||||
<div class="center-column"> |
||||
<%= textarea f, :constructor_arguments, class: "form-control border-rounded monospace", rows: 3, "aria-describedby": "contract-constructor-arguments-help-block" %> |
||||
<%= error_tag f, :constructor_arguments, id: "contract-constructor-arguments-help-block", class: "text-danger form-error", "data-test": "contract-constructor-arguments-error" %> |
||||
</div> |
||||
<div class="smart-contract-form-group-tooltip">Add arguments in <a href="https://solidity.readthedocs.io/en/develop/abi-spec.html" target="_blank">ABI hex encoded form</a>. Constructor arguments are written right to left, and will be found at the end of the input created bytecode. They may also be <a href="https://abi.hashex.org/" target="_blank">parsed here.</a></div> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="smart-contract-form-buttons"> |
||||
<button |
||||
class="position-absolute w-118 btn-full-primary d-none mr-2" |
||||
disabled="true" |
||||
id="loading" |
||||
name="button" |
||||
type="button" |
||||
> |
||||
<span class="loading-spinner-small mr-2"> |
||||
<span class="loading-spinner-block-1"></span> |
||||
<span class="loading-spinner-block-2"></span> |
||||
</span> |
||||
<%= gettext("Loading....") %> |
||||
</button> |
||||
<%= submit gettext("Verify & publish"), class: "btn-full-primary mr-2", "data-button-loading": "animation" %> |
||||
<%= reset gettext("Reset"), class: "btn-line mr-2 js-smart-contract-form-reset" %> |
||||
<%= |
||||
link( |
||||
gettext("Cancel"), |
||||
class: "btn-no-border", |
||||
to: address_contract_path(@conn, :index, @address_hash) |
||||
) |
||||
%> |
||||
</div> |
||||
<% end %> |
||||
</div> |
||||
<script defer data-cfasync="false" src="<%= static_path(@conn, "/js/verification-form.js") %>"></script> |
||||
</section> |
@ -0,0 +1,5 @@ |
||||
defmodule BlockScoutWeb.AddressContractVerificationVyperView do |
||||
use BlockScoutWeb, :view |
||||
|
||||
alias Explorer.Chain |
||||
end |
@ -1,2 +1,3 @@ |
||||
priv/.recovery |
||||
priv/solc_compilers/ |
||||
priv/vyper_compilers/ |
||||
|
@ -0,0 +1,197 @@ |
||||
defmodule Explorer.SmartContract.CompilerVersion do |
||||
@moduledoc """ |
||||
Adapter for fetching compiler versions from https://solc-bin.ethereum.org/bin/list.json. |
||||
""" |
||||
|
||||
@unsupported_solc_versions ~w(0.1.1 0.1.2) |
||||
@unsupported_vyper_versions ~w(v0.2.9 v0.2.10) |
||||
|
||||
@doc """ |
||||
Fetches a list of compilers from the Ethereum Solidity API. |
||||
""" |
||||
@spec fetch_versions(:solc | :vyper) :: {atom, [map]} |
||||
def fetch_versions(compiler) do |
||||
case compiler do |
||||
:solc -> fetch_solc_versions() |
||||
:vyper -> fetch_vyper_versions() |
||||
end |
||||
end |
||||
|
||||
defp fetch_solc_versions do |
||||
headers = [{"Content-Type", "application/json"}] |
||||
|
||||
case HTTPoison.get(source_url(:solc), headers) do |
||||
{:ok, %{status_code: 200, body: body}} -> |
||||
{:ok, format_data(body, :solc)} |
||||
|
||||
{:ok, %{status_code: _status_code, body: body}} -> |
||||
{:error, decode_json(body)["error"]} |
||||
|
||||
{:error, %{reason: reason}} -> |
||||
{:error, reason} |
||||
end |
||||
end |
||||
|
||||
defp fetch_vyper_versions do |
||||
headers = [{"Content-Type", "application/json"}] |
||||
|
||||
case HTTPoison.get(source_url(:vyper), headers) do |
||||
{:ok, %{status_code: 200, body: body}} -> |
||||
{:ok, format_data(body, :vyper)} |
||||
|
||||
{:ok, %{status_code: _status_code, body: body}} -> |
||||
{:error, decode_json(body)["error"]} |
||||
|
||||
{:error, %{reason: reason}} -> |
||||
{:error, reason} |
||||
end |
||||
end |
||||
|
||||
@spec vyper_releases_url :: String.t() |
||||
def vyper_releases_url do |
||||
"https://api.github.com/repos/vyperlang/vyper/releases" |
||||
end |
||||
|
||||
defp format_data(json, compiler) do |
||||
versions = |
||||
case compiler do |
||||
:solc -> |
||||
json |
||||
|> Jason.decode!() |
||||
|> Map.fetch!("builds") |
||||
|> remove_unsupported_versions(compiler) |
||||
|> format_versions(compiler) |
||||
|> Enum.reverse() |
||||
|
||||
:vyper -> |
||||
json |
||||
|> Jason.decode!() |
||||
|> remove_unsupported_versions(compiler) |
||||
|> format_versions(compiler) |
||||
|> Enum.sort(fn version1, version2 -> |
||||
versions1 = String.split(version1, ".") |
||||
versions2 = String.split(version2, ".") |
||||
major1 = versions1 |> Enum.at(0) |> parse_integer() |
||||
major2 = versions2 |> Enum.at(0) |> parse_integer() |
||||
minor1 = versions1 |> Enum.at(1) |> parse_integer() |
||||
minor2 = versions2 |> Enum.at(1) |> parse_integer() |
||||
patch1 = versions1 |> Enum.at(2) |> String.split("-") |> Enum.at(0) |> parse_integer() |
||||
patch2 = versions2 |> Enum.at(2) |> String.split("-") |> Enum.at(0) |> parse_integer() |
||||
major1 >= major2 && minor1 >= minor2 && patch1 >= patch2 |
||||
end) |
||||
end |
||||
|
||||
["latest" | versions] |
||||
end |
||||
|
||||
defp parse_integer(string) do |
||||
case Integer.parse(string) do |
||||
{number, ""} -> number |
||||
_ -> nil |
||||
end |
||||
end |
||||
|
||||
@spec remove_unsupported_versions([String.t()], :solc | :vyper) :: [String.t()] |
||||
defp remove_unsupported_versions(builds, compiler) do |
||||
case compiler do |
||||
:solc -> |
||||
Enum.reject(builds, fn %{"version" => version} -> |
||||
Enum.member?(@unsupported_solc_versions, version) |
||||
end) |
||||
|
||||
:vyper -> |
||||
Enum.reject(builds, fn %{"tag_name" => version} -> |
||||
Enum.member?(@unsupported_vyper_versions, version) |
||||
end) |
||||
end |
||||
end |
||||
|
||||
defp format_versions(builds, compiler) do |
||||
case compiler do |
||||
:solc -> |
||||
Enum.map(builds, fn build -> |
||||
build |
||||
|> Map.fetch!("path") |
||||
|> String.replace_prefix("soljson-", "") |
||||
|> String.replace_suffix(".js", "") |
||||
end) |
||||
|
||||
:vyper -> |
||||
Enum.map(builds, fn build -> |
||||
build |
||||
|> Map.fetch!("tag_name") |
||||
end) |
||||
end |
||||
end |
||||
|
||||
defp decode_json(json) do |
||||
Jason.decode!(json) |
||||
end |
||||
|
||||
@spec source_url(:solc | :vyper) :: String.t() |
||||
defp source_url(compiler) do |
||||
case compiler do |
||||
:solc -> |
||||
solc_bin_api_url = Application.get_env(:explorer, :solc_bin_api_url) |
||||
"#{solc_bin_api_url}/bin/list.json" |
||||
|
||||
:vyper -> |
||||
vyper_releases_url() |
||||
end |
||||
end |
||||
|
||||
def get_strict_compiler_version(compiler, compiler_version) do |
||||
case compiler do |
||||
:solc -> |
||||
if compiler_version == "latest" do |
||||
compiler_versions = get_compiler_versions(:solc) |
||||
|
||||
if Enum.count(compiler_versions) > 1 do |
||||
latest_stable_version = |
||||
compiler_versions |
||||
|> Enum.drop(1) |
||||
|> Enum.reduce_while("", fn version, acc -> |
||||
if String.contains?(version, "-nightly") do |
||||
{:cont, acc} |
||||
else |
||||
{:halt, version} |
||||
end |
||||
end) |
||||
|
||||
latest_stable_version |
||||
else |
||||
"latest" |
||||
end |
||||
else |
||||
compiler_version |
||||
end |
||||
|
||||
:vyper -> |
||||
if compiler_version == "latest" do |
||||
compiler_versions = get_compiler_versions(:vyper) |
||||
|
||||
if Enum.count(compiler_versions) > 1 do |
||||
latest_stable_version = |
||||
compiler_versions |
||||
|> Enum.at(1) |
||||
|
||||
latest_stable_version |
||||
else |
||||
"latest" |
||||
end |
||||
else |
||||
compiler_version |
||||
end |
||||
end |
||||
end |
||||
|
||||
defp get_compiler_versions(compiler) do |
||||
case fetch_versions(compiler) do |
||||
{:ok, compiler_versions} -> |
||||
compiler_versions |
||||
|
||||
{:error, _} -> |
||||
[] |
||||
end |
||||
end |
||||
end |
@ -1,95 +0,0 @@ |
||||
defmodule Explorer.SmartContract.Solidity.CompilerVersion do |
||||
@moduledoc """ |
||||
Adapter for fetching compiler versions from https://solc-bin.ethereum.org/bin/list.json. |
||||
""" |
||||
|
||||
@unsupported_versions ~w(0.1.1 0.1.2) |
||||
|
||||
@doc """ |
||||
Fetches a list of compilers from the Ethereum Solidity API. |
||||
""" |
||||
@spec fetch_versions :: {atom, [map]} |
||||
def fetch_versions do |
||||
headers = [{"Content-Type", "application/json"}] |
||||
|
||||
case HTTPoison.get(source_url(), headers) do |
||||
{:ok, %{status_code: 200, body: body}} -> |
||||
{:ok, format_data(body)} |
||||
|
||||
{:ok, %{status_code: _status_code, body: body}} -> |
||||
{:error, decode_json(body)["error"]} |
||||
|
||||
{:error, %{reason: reason}} -> |
||||
{:error, reason} |
||||
end |
||||
end |
||||
|
||||
defp format_data(json) do |
||||
versions = |
||||
json |
||||
|> Jason.decode!() |
||||
|> Map.fetch!("builds") |
||||
|> remove_unsupported_versions() |
||||
|> format_versions() |
||||
|> Enum.reverse() |
||||
|
||||
["latest" | versions] |
||||
end |
||||
|
||||
defp remove_unsupported_versions(builds) do |
||||
Enum.reject(builds, fn %{"version" => version} -> |
||||
Enum.member?(@unsupported_versions, version) |
||||
end) |
||||
end |
||||
|
||||
defp format_versions(builds) do |
||||
Enum.map(builds, fn build -> |
||||
build |
||||
|> Map.fetch!("path") |
||||
|> String.replace_prefix("soljson-", "") |
||||
|> String.replace_suffix(".js", "") |
||||
end) |
||||
end |
||||
|
||||
defp decode_json(json) do |
||||
Jason.decode!(json) |
||||
end |
||||
|
||||
defp source_url do |
||||
solc_bin_api_url = Application.get_env(:explorer, :solc_bin_api_url) |
||||
|
||||
"#{solc_bin_api_url}/bin/list.json" |
||||
end |
||||
|
||||
def get_strict_compiler_version(compiler_version) do |
||||
if compiler_version == "latest" do |
||||
compiler_versions = |
||||
case fetch_versions() do |
||||
{:ok, compiler_versions} -> |
||||
compiler_versions |
||||
|
||||
{:error, _} -> |
||||
[] |
||||
end |
||||
|
||||
if Enum.count(compiler_versions) > 1 do |
||||
latest_stable_version = |
||||
compiler_versions |
||||
|> Enum.drop(1) |
||||
|> Enum.reduce_while("", fn version, acc -> |
||||
if String.contains?(version, "-nightly") do |
||||
{:cont, acc} |
||||
else |
||||
{:halt, version} |
||||
end |
||||
end) |
||||
|
||||
latest_stable_version |
||||
else |
||||
"latest" |
||||
end |
||||
else |
||||
compiler_version |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,74 @@ |
||||
defmodule Explorer.SmartContract.Vyper.CodeCompiler do |
||||
@moduledoc """ |
||||
Module responsible to compile the Vyper code of a given Smart Contract. |
||||
""" |
||||
|
||||
alias Explorer.SmartContract.VyperDownloader |
||||
|
||||
require Logger |
||||
|
||||
@spec run(Keyword.t()) :: {:ok, map} | {:error, :compilation | :name} |
||||
def run(params) do |
||||
compiler_version = Keyword.fetch!(params, :compiler_version) |
||||
code = Keyword.fetch!(params, :code) |
||||
|
||||
path = VyperDownloader.ensure_exists(compiler_version) |
||||
|
||||
source_file_path = create_source_file(code) |
||||
|
||||
if path do |
||||
{response, _status} = |
||||
System.cmd( |
||||
path, |
||||
[ |
||||
"-f", |
||||
"abi,bytecode", |
||||
source_file_path |
||||
] |
||||
) |
||||
|
||||
response_data = String.split(response, "\n") |
||||
abi_row = response_data |> Enum.at(0) |
||||
bytecode = response_data |> Enum.at(1) |
||||
|
||||
case Jason.decode(abi_row) do |
||||
{:ok, abi} -> |
||||
{:ok, %{"abi" => abi, "bytecode" => bytecode}} |
||||
|
||||
{:error, %Jason.DecodeError{}} -> |
||||
{:error, :compilation} |
||||
end |
||||
else |
||||
{:error, :compilation} |
||||
end |
||||
end |
||||
|
||||
def get_contract_info(contracts, _) when contracts == %{}, do: {:error, :compilation} |
||||
|
||||
def get_contract_info(contracts, name) do |
||||
new_versions_name = ":" <> name |
||||
|
||||
case contracts do |
||||
%{^new_versions_name => response} -> |
||||
response |
||||
|
||||
%{^name => response} -> |
||||
response |
||||
|
||||
_ -> |
||||
{:error, :name} |
||||
end |
||||
end |
||||
|
||||
def parse_error({:error, %{"error" => error}}), do: {:error, [error]} |
||||
def parse_error({:error, %{"errors" => errors}}), do: {:error, errors} |
||||
def parse_error({:error, _} = error), do: error |
||||
|
||||
defp create_source_file(source) do |
||||
{:ok, path} = Briefly.create() |
||||
|
||||
File.write!(path, source) |
||||
|
||||
path |
||||
end |
||||
end |
@ -0,0 +1,69 @@ |
||||
defmodule Explorer.SmartContract.Vyper.Publisher do |
||||
@moduledoc """ |
||||
Module responsible to control Vyper contract verification. |
||||
""" |
||||
|
||||
alias Explorer.Chain |
||||
alias Explorer.Chain.SmartContract |
||||
alias Explorer.SmartContract.CompilerVersion |
||||
alias Explorer.SmartContract.Vyper.Verifier |
||||
|
||||
def publish(address_hash, params) do |
||||
case Verifier.evaluate_authenticity(address_hash, params) do |
||||
{:ok, %{abi: abi}} -> |
||||
publish_smart_contract(address_hash, params, abi) |
||||
|
||||
{:error, error} -> |
||||
{:error, unverified_smart_contract(address_hash, params, error, nil)} |
||||
end |
||||
end |
||||
|
||||
def publish_smart_contract(address_hash, params, abi) do |
||||
attrs = address_hash |> attributes(params, abi) |
||||
|
||||
Chain.create_smart_contract(attrs, attrs.external_libraries, attrs.secondary_sources) |
||||
end |
||||
|
||||
defp unverified_smart_contract(address_hash, params, error, error_message) do |
||||
attrs = attributes(address_hash, params) |
||||
|
||||
changeset = |
||||
SmartContract.invalid_contract_changeset( |
||||
%SmartContract{address_hash: address_hash}, |
||||
attrs, |
||||
error, |
||||
error_message |
||||
) |
||||
|
||||
%{changeset | action: :insert} |
||||
end |
||||
|
||||
defp attributes(address_hash, params, abi \\ %{}) do |
||||
constructor_arguments = params["constructor_arguments"] |
||||
|
||||
clean_constructor_arguments = |
||||
if constructor_arguments != nil && constructor_arguments != "" do |
||||
constructor_arguments |
||||
else |
||||
nil |
||||
end |
||||
|
||||
compiler_version = CompilerVersion.get_strict_compiler_version(:vyper, params["compiler_version"]) |
||||
|
||||
%{ |
||||
address_hash: address_hash, |
||||
name: "Vyper_contract", |
||||
compiler_version: compiler_version, |
||||
evm_version: nil, |
||||
optimization_runs: nil, |
||||
optimization: false, |
||||
contract_source_code: params["contract_source_code"], |
||||
constructor_arguments: clean_constructor_arguments, |
||||
external_libraries: [], |
||||
secondary_sources: [], |
||||
abi: abi, |
||||
verified_via_sourcify: false, |
||||
is_vyper_contract: true |
||||
} |
||||
end |
||||
end |
@ -0,0 +1,23 @@ |
||||
defmodule Explorer.SmartContract.Vyper.PublisherWorker do |
||||
@moduledoc """ |
||||
Background smart contract verification worker. |
||||
""" |
||||
|
||||
use Que.Worker, concurrency: 5 |
||||
|
||||
alias Explorer.Chain.Events.Publisher, as: EventsPublisher |
||||
alias Explorer.SmartContract.Vyper.Publisher |
||||
|
||||
def perform({address_hash, params, conn}) do |
||||
result = |
||||
case Publisher.publish(address_hash, params) do |
||||
{:ok, _contract} = result -> |
||||
result |
||||
|
||||
{:error, changeset} -> |
||||
{:error, changeset} |
||||
end |
||||
|
||||
EventsPublisher.broadcast([{:contract_verification_result, {address_hash, result, conn}}], :on_demand) |
||||
end |
||||
end |
@ -0,0 +1,65 @@ |
||||
# credo:disable-for-this-file |
||||
defmodule Explorer.SmartContract.Vyper.Verifier do |
||||
@moduledoc """ |
||||
Module responsible to verify the Smart Contract through Vyper. |
||||
|
||||
Given a contract source code the bytecode will be generated and matched |
||||
against the existing Creation Address Bytecode, if it matches the contract is |
||||
then Verified. |
||||
""" |
||||
|
||||
alias Explorer.Chain |
||||
alias Explorer.SmartContract.Vyper.CodeCompiler |
||||
|
||||
def evaluate_authenticity(_, %{"name" => ""}), do: {:error, :name} |
||||
|
||||
def evaluate_authenticity(_, %{"contract_source_code" => ""}), |
||||
do: {:error, :contract_source_code} |
||||
|
||||
def evaluate_authenticity(address_hash, params) do |
||||
verify(address_hash, params) |
||||
end |
||||
|
||||
defp verify(address_hash, params) do |
||||
contract_source_code = Map.fetch!(params, "contract_source_code") |
||||
compiler_version = Map.fetch!(params, "compiler_version") |
||||
constructor_arguments = Map.get(params, "constructor_arguments", "") |
||||
|
||||
vyper_output = |
||||
CodeCompiler.run( |
||||
compiler_version: compiler_version, |
||||
code: contract_source_code |
||||
) |
||||
|
||||
compare_bytecodes( |
||||
vyper_output, |
||||
address_hash, |
||||
constructor_arguments |
||||
) |
||||
end |
||||
|
||||
defp compare_bytecodes({:error, _}, _, _), do: {:error, :compilation} |
||||
|
||||
# credo:disable-for-next-line /Complexity/ |
||||
defp compare_bytecodes( |
||||
{:ok, %{"abi" => abi, "bytecode" => bytecode}}, |
||||
address_hash, |
||||
arguments_data |
||||
) do |
||||
blockchain_bytecode = |
||||
case Chain.smart_contract_creation_tx_bytecode(address_hash) do |
||||
%{init: init, created_contract_code: _created_contract_code} -> |
||||
init |
||||
|
||||
_ -> |
||||
nil |
||||
end |
||||
|> String.trim() |
||||
|
||||
if String.trim(bytecode <> arguments_data) == blockchain_bytecode do |
||||
{:ok, %{abi: abi}} |
||||
else |
||||
{:error, :generated_bytecode} |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,127 @@ |
||||
defmodule Explorer.SmartContract.VyperDownloader do |
||||
@moduledoc """ |
||||
Checks to see if the requested Vyper compiler version exists, and if not it |
||||
downloads and stores the file. |
||||
""" |
||||
use GenServer |
||||
|
||||
alias Explorer.SmartContract.CompilerVersion |
||||
|
||||
@latest_compiler_refetch_time :timer.minutes(30) |
||||
|
||||
def ensure_exists(version) do |
||||
path = file_path(version) |
||||
|
||||
if File.exists?(path) && version !== "latest" do |
||||
path |
||||
else |
||||
compiler_versions = |
||||
case CompilerVersion.fetch_versions(:vyper) do |
||||
{:ok, compiler_versions} -> |
||||
compiler_versions |
||||
|
||||
{:error, _} -> |
||||
[] |
||||
end |
||||
|
||||
if version in compiler_versions do |
||||
GenServer.call(__MODULE__, {:ensure_exists, version}, 60_000) |
||||
else |
||||
false |
||||
end |
||||
end |
||||
end |
||||
|
||||
def start_link(_) do |
||||
GenServer.start_link(__MODULE__, [], name: __MODULE__) |
||||
end |
||||
|
||||
# sobelow_skip ["Traversal"] |
||||
@impl true |
||||
def init([]) do |
||||
File.mkdir(compiler_dir()) |
||||
|
||||
{:ok, []} |
||||
end |
||||
|
||||
# sobelow_skip ["Traversal"] |
||||
@impl true |
||||
def handle_call({:ensure_exists, version}, _from, state) do |
||||
path = file_path(version) |
||||
|
||||
if fetch?(version, path) do |
||||
temp_path = file_path("#{version}-tmp") |
||||
|
||||
contents = download(version) |
||||
|
||||
file = File.open!(temp_path, [:write, :exclusive]) |
||||
|
||||
IO.binwrite(file, contents) |
||||
File.close(file) |
||||
|
||||
File.rename(temp_path, path) |
||||
System.cmd("chmod", ["+x", path]) |
||||
end |
||||
|
||||
{:reply, path, state} |
||||
end |
||||
|
||||
defp fetch?("latest", path) do |
||||
case File.stat(path) do |
||||
{:error, :enoent} -> |
||||
true |
||||
|
||||
{:ok, %{mtime: mtime}} -> |
||||
last_modified = NaiveDateTime.from_erl!(mtime) |
||||
diff = Timex.diff(NaiveDateTime.utc_now(), last_modified, :milliseconds) |
||||
|
||||
diff > @latest_compiler_refetch_time |
||||
end |
||||
end |
||||
|
||||
defp fetch?(_, path) do |
||||
not File.exists?(path) |
||||
end |
||||
|
||||
defp file_path(version) do |
||||
Path.join(compiler_dir(), "#{version}") |
||||
end |
||||
|
||||
defp compiler_dir do |
||||
Application.app_dir(:explorer, "priv/vyper_compilers/") |
||||
end |
||||
|
||||
defp download(version) do |
||||
version = CompilerVersion.get_strict_compiler_version(:vyper, version) |
||||
releases_path = CompilerVersion.vyper_releases_url() |
||||
|
||||
releases_body = |
||||
releases_path |
||||
|> HTTPoison.get!([], timeout: 60_000, recv_timeout: 60_000) |
||||
|> Map.get(:body) |
||||
|> Jason.decode!() |
||||
|
||||
release = |
||||
releases_body |
||||
|> Enum.find(fn release -> |
||||
Map.get(release, "tag_name") == version |
||||
end) |
||||
|
||||
release_assets = Map.get(release, "assets") |
||||
|
||||
download_path = |
||||
Enum.reduce_while(release_assets, "", fn asset, acc -> |
||||
browser_download_url = Map.get(asset, "browser_download_url") |
||||
|
||||
if browser_download_url =~ "linux" do |
||||
{:halt, browser_download_url} |
||||
else |
||||
{:cont, acc} |
||||
end |
||||
end) |
||||
|
||||
download_path |
||||
|> HTTPoison.get!([], timeout: 60_000, recv_timeout: 60_000, follow_redirect: true, hackney: [force_redirect: true]) |
||||
|> Map.get(:body) |
||||
end |
||||
end |
@ -0,0 +1,9 @@ |
||||
defmodule Explorer.Repo.Migrations.SmartContractsAddIsVyperFlag do |
||||
use Ecto.Migration |
||||
|
||||
def change do |
||||
alter table(:smart_contracts) do |
||||
add(:is_vyper_contract, :boolean, null: true) |
||||
end |
||||
end |
||||
end |
@ -1,12 +1,12 @@ |
||||
defmodule Explorer.SmartContract.VerifierTest do |
||||
defmodule Explorer.SmartContract.Solidity.VerifierTest do |
||||
use ExUnit.Case, async: true |
||||
use Explorer.DataCase |
||||
|
||||
@moduletag timeout: :infinity |
||||
|
||||
doctest Explorer.SmartContract.Verifier |
||||
doctest Explorer.SmartContract.Solidity.Verifier |
||||
|
||||
alias Explorer.SmartContract.Verifier |
||||
alias Explorer.SmartContract.Solidity.Verifier |
||||
alias Explorer.Factory |
||||
|
||||
@code_0_4 """ |
Loading…
Reference in new issue