feat: Blueprint contracts support (#10058)

* Update smart-contract to have 'is_blueprint' flag

* Store into the database 'is_blueprint' value retrieved from verification or lookup results

* Make use of TestHelper module for smart_contract_controller_test

* Make use of TestHelper module for verification_controller_test. Fix invalid 'is_blueprint' value in fixture

* Fix spelling. Simplify Vyper.publish method

* Add 'Averify' into spelling check

* Add PR url to the comment with rationale behind url-encoding ':' symbol
mf-only-health-webapp
Rim Rakhimov 6 months ago committed by GitHub
parent 64b55faea3
commit fcc5ff2c89
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex
  2. 80
      apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs
  3. 76
      apps/block_scout_web/test/block_scout_web/controllers/api/v2/verification_controller_test.exs
  4. 21
      apps/block_scout_web/test/support/fixture/smart_contract/smart_contract_verifier_vyper_multi_part_blueprint_response.json
  5. 8
      apps/explorer/lib/explorer/chain/smart_contract.ex
  6. 12
      apps/explorer/lib/explorer/smart_contract/rust_verifier_interface_behaviour.ex
  7. 4
      apps/explorer/lib/explorer/smart_contract/solidity/publisher.ex
  8. 19
      apps/explorer/lib/explorer/smart_contract/vyper/publisher.ex
  9. 9
      apps/explorer/priv/repo/migrations/20240509014500_smart_contracts_add_is_blueprint_flag.exs
  10. 1
      cspell.json

@ -216,7 +216,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do
),
"language" => smart_contract_language(smart_contract),
"license_type" => smart_contract.license_type,
"certified" => if(smart_contract.certified, do: smart_contract.certified, else: false)
"certified" => if(smart_contract.certified, do: smart_contract.certified, else: false),
"is_blueprint" => if(smart_contract.is_blueprint, do: smart_contract.is_blueprint, else: false)
}
|> Map.merge(bytecode_info(address))
end

@ -155,7 +155,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do
"is_verified_via_verifier_alliance" => target_contract.verified_via_verifier_alliance,
"language" => smart_contract_language(target_contract),
"license_type" => "none",
"certified" => false
"certified" => false,
"is_blueprint" => false
}
implementation_address = insert(:address)
@ -260,7 +261,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do
"is_verified_via_verifier_alliance" => target_contract.verified_via_verifier_alliance,
"language" => smart_contract_language(target_contract),
"license_type" => "gnu_agpl_v3",
"certified" => false
"certified" => false,
"is_blueprint" => false
}
TestHelper.get_eip1967_implementation_error_response()
@ -365,7 +367,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do
"is_verified_via_verifier_alliance" => target_contract.verified_via_verifier_alliance,
"language" => smart_contract_language(target_contract),
"license_type" => "none",
"certified" => false
"certified" => false,
"is_blueprint" => false
}
TestHelper.get_eip1967_implementation_zero_addresses()
@ -486,7 +489,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do
"is_verified_via_verifier_alliance" => implementation_contract.verified_via_verifier_alliance,
"language" => smart_contract_language(implementation_contract),
"license_type" => "bsd_3_clause",
"certified" => false
"certified" => false,
"is_blueprint" => false
}
request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(proxy_address.hash)}")
@ -494,6 +498,74 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do
assert correct_response == response
end
test "get smart-contract which is blueprint", %{conn: conn} do
target_contract =
insert(:smart_contract,
is_blueprint: true
)
insert(:transaction,
created_contract_address_hash: target_contract.address_hash,
input:
"0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029"
)
|> with_block()
correct_response = %{
"verified_twin_address_hash" => nil,
"is_verified" => true,
"is_changed_bytecode" => false,
"is_partially_verified" => target_contract.partially_verified,
"is_fully_verified" => true,
"is_verified_via_sourcify" => target_contract.verified_via_sourcify,
"is_vyper_contract" => target_contract.is_vyper_contract,
"has_methods_read" => true,
"has_methods_write" => true,
"has_methods_read_proxy" => false,
"has_methods_write_proxy" => false,
"has_custom_methods_read" => false,
"has_custom_methods_write" => false,
"minimal_proxy_address_hash" => nil,
"sourcify_repo_url" =>
if(target_contract.verified_via_sourcify,
do: AddressContractView.sourcify_repo_url(target_contract.address_hash, target_contract.partially_verified)
),
"can_be_visualized_via_sol2uml" => false,
"name" => target_contract && target_contract.name,
"compiler_version" => target_contract.compiler_version,
"optimization_enabled" => target_contract.optimization,
"optimization_runs" => target_contract.optimization_runs,
"evm_version" => target_contract.evm_version,
"verified_at" => target_contract.inserted_at |> to_string() |> String.replace(" ", "T"),
"source_code" => target_contract.contract_source_code,
"file_path" => target_contract.file_path,
"additional_sources" => [],
"compiler_settings" => target_contract.compiler_settings,
"external_libraries" => target_contract.external_libraries,
"constructor_args" => target_contract.constructor_arguments,
"decoded_constructor_args" => nil,
"is_self_destructed" => false,
"deployed_bytecode" =>
"0x6080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029",
"creation_bytecode" =>
"0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029",
"abi" => target_contract.abi,
"is_verified_via_eth_bytecode_db" => target_contract.verified_via_eth_bytecode_db,
"is_verified_via_verifier_alliance" => target_contract.verified_via_verifier_alliance,
"language" => smart_contract_language(target_contract),
"license_type" => "none",
"certified" => false,
"is_blueprint" => true
}
TestHelper.get_eip1967_implementation_zero_addresses()
request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(target_contract.address_hash)}")
response = json_response(request, 200)
assert correct_response == response
end
end
test "get smart-contract implementation for 'Clones with immutable arguments' pattern", %{conn: conn} do

@ -3,7 +3,10 @@ defmodule BlockScoutWeb.API.V2.VerificationControllerTest do
use BlockScoutWeb.ChannelCase, async: false
alias BlockScoutWeb.UserSocketV2
alias Explorer.Chain.Address
alias Explorer.TestHelper
alias Tesla.Multipart
alias Plug.Conn
@moduletag timeout: :infinity
@ -349,6 +352,79 @@ defmodule BlockScoutWeb.API.V2.VerificationControllerTest do
Application.put_env(:explorer, :solc_bin_api_url, before)
end
test "blueprint contract verification", %{conn: conn} do
bypass = Bypass.open()
sc_verifier_response =
File.read!(
"./test/support/fixture/smart_contract/smart_contract_verifier_vyper_multi_part_blueprint_response.json"
)
old_env = Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour)
Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour,
service_url: "http://localhost:#{bypass.port}",
enabled: true,
type: "sc_verifier",
eth_bytecode_db?: true
)
Bypass.expect_once(bypass, "POST", "/api/v2//verifier/vyper/sources%3Averify-multi-part", fn conn ->
Conn.resp(conn, 200, sc_verifier_response)
end)
bytecode =
"0xfe7100346100235760206100995f395f516001555f5f5561005f61002760003961005f6000f35b5f80fd5f3560e01c60026001821660011b61005b01601e395f51565b63158ef93e81186100535734610057575f5460405260206040f3610053565b633fa4f245811861005357346100575760015460405260206040f35b5f5ffd5b5f80fd0018003784185f810400a16576797065728300030a0013"
input =
"0x61009c3d81600a3d39f3fe7100346100235760206100995f395f516001555f5f5561005f61002760003961005f6000f35b5f80fd5f3560e01c60026001821660011b61005b01601e395f51565b63158ef93e81186100535734610057575f5460405260206040f3610053565b633fa4f245811861005357346100575760015460405260206040f35b5f5ffd5b5f80fd0018003784185f810400a16576797065728300030a0013"
contract_address = insert(:contract_address, contract_code: bytecode)
:transaction
|> insert(
created_contract_address_hash: contract_address.hash,
input: input
)
|> with_block(status: :ok)
topic = "addresses:#{contract_address.hash}"
{:ok, _reply, _socket} =
BlockScoutWeb.UserSocketV2
|> socket("no_id", %{})
|> subscribe_and_join(topic)
# We can actually use any params here, as verification service response is defined in `sc_verifier_response`
params = %{
"source_code" => "some_valid_source_code",
"compiler_version" => "v0.3.10",
"contract_name" => "abc"
}
request = post(conn, "/api/v2/smart-contracts/#{contract_address.hash}/verification/via/vyper-code", params)
assert %{"message" => "Smart-contract verification started"} = json_response(request, 200)
assert_receive %Phoenix.Socket.Message{
payload: %{status: "success"},
event: "verification_result",
topic: ^topic
},
:timer.seconds(300)
# Assert that the `is_blueprint=true` is stored in the database after verification
TestHelper.get_eip1967_implementation_zero_addresses()
request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(contract_address.hash)}")
response = json_response(request, 200)
assert response["is_blueprint"] == true
Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, old_env)
Bypass.down(bypass)
end
end
describe "/api/v2/smart-contracts/{address_hash}/verification/via/vyper-multi-part" do

@ -0,0 +1,21 @@
{
"message": "OK",
"status": "SUCCESS",
"source": {
"fileName": "Test.vy",
"contractName": "Test",
"compilerVersion": "v0.3.10+commit.91361694",
"compilerSettings": "{\"outputSelection\":{\"*\":[\"abi\",\"evm.bytecode\",\"evm.deployedBytecode\",\"evm.methodIdentifiers\"]}}",
"sourceType": "VYPER",
"sourceFiles": {
"Test.vy": "initialized: public(bool)\nvalue: public(uint256)\n\n@external\ndef __init__(_value: uint256):\n self.value = _value\n self.initialized = False"
},
"abi": "[{\"inputs\":[{\"name\":\"_value\",\"type\":\"uint256\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[],\"name\":\"initialized\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"value\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]",
"constructorArguments": null,
"matchType": "PARTIAL",
"compilationArtifacts": "{\"abi\":[{\"inputs\":[{\"name\":\"_value\",\"type\":\"uint256\"}],\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[],\"name\":\"initialized\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"value\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"}],\"sources\":{\"Test.vy\":{\"id\":0}}}",
"creationInputArtifacts": "{}",
"deployedBytecodeArtifacts": "{\"sourceMap\":\"-1:-1:0:-;;;;;;;;;;;;;;;;:::-;:::-;;;;;:::-;;;:::-;;;;;;20:4;;-1:-1;:::-;:::-;;;;;:::-;;;:::-;;;;;;40:7;;-1:-1::-;;;;:::-;;;\"}",
"isBlueprint": true
}
}

@ -270,6 +270,7 @@ defmodule Explorer.Chain.SmartContract do
* `autodetect_constructor_args` - field was added for storing user's choice
* `is_yul` - field was added for storing user's choice
* `certified` - boolean flag, which can be set for set of smart-contracts via runtime env variable to prioritize those smart-contracts in the search.
* `is_blueprint` - boolean flag, determines if contract is ERC-5202 compatible blueprint contract or not.
"""
typed_schema "smart_contracts" do
field(:name, :string, null: false)
@ -296,6 +297,7 @@ defmodule Explorer.Chain.SmartContract do
field(:metadata_from_verified_bytecode_twin, :boolean, virtual: true)
field(:license_type, Ecto.Enum, values: @license_enum, default: :none)
field(:certified, :boolean)
field(:is_blueprint, :boolean)
has_many(
:decompiled_smart_contracts,
@ -347,7 +349,8 @@ defmodule Explorer.Chain.SmartContract do
:contract_code_md5,
:compiler_settings,
:license_type,
:certified
:certified,
:is_blueprint
])
|> validate_required([
:name,
@ -390,7 +393,8 @@ defmodule Explorer.Chain.SmartContract do
:contract_code_md5,
:autodetect_constructor_args,
:license_type,
:certified
:certified,
:is_blueprint
])
|> (&if(verification_with_files?,
do: &1,

@ -154,16 +154,20 @@ defmodule Explorer.SmartContract.RustVerifierInterfaceBehaviour do
def process_verifier_response(other, _), do: {:error, other}
# Uses url encoded ("%3A") version of ':', as ':' symbol breaks `Bypass` library during tests.
# https://github.com/PSPDFKit-labs/bypass/issues/122
def solidity_multiple_files_verification_url,
do: "#{base_api_url()}" <> "/verifier/solidity/sources:verify-multi-part"
do: "#{base_api_url()}" <> "/verifier/solidity/sources%3Averify-multi-part"
def vyper_multiple_files_verification_url, do: "#{base_api_url()}" <> "/verifier/vyper/sources:verify-multi-part"
def vyper_multiple_files_verification_url,
do: "#{base_api_url()}" <> "/verifier/vyper/sources%3Averify-multi-part"
def vyper_standard_json_verification_url,
do: "#{base_api_url()}" <> "/verifier/vyper/sources:verify-standard-json"
do: "#{base_api_url()}" <> "/verifier/vyper/sources%3Averify-standard-json"
def solidity_standard_json_verification_url,
do: "#{base_api_url()}" <> "/verifier/solidity/sources:verify-standard-json"
do: "#{base_api_url()}" <> "/verifier/solidity/sources%3Averify-standard-json"
def versions_list_url, do: "#{base_api_url()}" <> "/verifier/solidity/versions"

@ -180,6 +180,7 @@ defmodule Explorer.SmartContract.Solidity.Publisher do
|> Map.put("verified_via_eth_bytecode_db", automatically_verified?)
|> Map.put("verified_via_verifier_alliance", source["verifier_alliance?"])
|> Map.put("license_type", initial_params["license_type"])
|> Map.put("is_blueprint", source["isBlueprint"])
publish_smart_contract(address_hash, prepared_params, Jason.decode!(abi_string || "null"))
end
@ -299,7 +300,8 @@ defmodule Explorer.SmartContract.Solidity.Publisher do
autodetect_constructor_args: params["autodetect_constructor_args"],
is_yul: params["is_yul"] || false,
compiler_settings: clean_compiler_settings,
license_type: prepare_license_type(params["license_type"]) || :none
license_type: prepare_license_type(params["license_type"]) || :none,
is_blueprint: params["is_blueprint"] || false
}
end

@ -118,6 +118,7 @@ defmodule Explorer.SmartContract.Vyper.Publisher do
)
|> Map.put("compiler_settings", if(standard_json?, do: compiler_settings))
|> Map.put("license_type", initial_params["license_type"])
|> Map.put("is_blueprint", source["isBlueprint"])
publish_smart_contract(address_hash, prepared_params, Jason.decode!(abi_string))
end
@ -186,12 +187,7 @@ defmodule Explorer.SmartContract.Vyper.Publisher do
constructor_arguments = params["constructor_arguments"]
compiler_settings = params["compiler_settings"]
clean_constructor_arguments =
if constructor_arguments != nil && constructor_arguments != "" do
constructor_arguments
else
nil
end
clean_constructor_arguments = clear_constructor_arguments(constructor_arguments)
clean_compiler_settings =
if compiler_settings in ["", nil, %{}] do
@ -223,7 +219,16 @@ defmodule Explorer.SmartContract.Vyper.Publisher do
is_vyper_contract: true,
file_path: params["file_path"],
compiler_settings: clean_compiler_settings,
license_type: prepare_license_type(params["license_type"]) || :none
license_type: prepare_license_type(params["license_type"]) || :none,
is_blueprint: params["is_blueprint"] || false
}
end
defp clear_constructor_arguments(constructor_arguments) do
if constructor_arguments != nil && constructor_arguments != "" do
constructor_arguments
else
nil
end
end
end

@ -0,0 +1,9 @@
defmodule Explorer.Repo.Migrations.SmartContractsAddIsBlueprintFlag do
use Ecto.Migration
def change do
alter table(:smart_contracts) do
add(:is_blueprint, :boolean, null: true)
end
end
end

@ -18,6 +18,7 @@
"Asfpp",
"Autodetection",
"Autonity",
"Averify",
"bitmask",
"Blockchair",
"CALLCODE",

Loading…
Cancel
Save