feat: Certified smart contracts (#9910)

* Certified smart-contracts

* Prioritize certified smart-contracts in the search

* Refactoring: remove CustomContractsHelper

* Return certified in the list and in the search

* mix format

* Fix tests

* Process review comment
pull/9929/head
Victor Baranov 7 months ago committed by GitHub
parent 942df2196f
commit cef3285999
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 17
      apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex
  2. 6
      apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex
  3. 12
      apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs
  4. 2
      apps/explorer/config/config.exs
  5. 1
      apps/explorer/lib/explorer/application.ex
  6. 4
      apps/explorer/lib/explorer/chain/search.ex
  7. 28
      apps/explorer/lib/explorer/chain/smart_contract.ex
  8. 6
      apps/explorer/lib/explorer/custom_contracts_helper.ex
  9. 29
      apps/explorer/lib/explorer/smart_contract/certified_smart_contract_cataloger.ex
  10. 13
      apps/explorer/priv/repo/migrations/20240417141515_smart_contracts_add_certified_flag.exs
  11. 19
      config/config_helper.exs
  12. 3
      config/runtime.exs
  13. 1
      docker-compose/envs/common-blockscout.env

@ -36,12 +36,25 @@ defmodule BlockScoutWeb.API.V2.SearchView do
"total_supply" => search_result.total_supply, "total_supply" => search_result.total_supply,
"circulating_market_cap" => "circulating_market_cap" =>
search_result.circulating_market_cap && to_string(search_result.circulating_market_cap), search_result.circulating_market_cap && to_string(search_result.circulating_market_cap),
"is_verified_via_admin_panel" => search_result.is_verified_via_admin_panel "is_verified_via_admin_panel" => search_result.is_verified_via_admin_panel,
"certified" => if(search_result.certified, do: search_result.certified, else: false)
}
end
def prepare_search_result(%{type: "contract"} = search_result) do
%{
"type" => search_result.type,
"name" => search_result.name,
"address" => search_result.address_hash,
"url" => address_path(Endpoint, :show, search_result.address_hash),
"is_smart_contract_verified" => search_result.verified,
"ens_info" => search_result[:ens_info],
"certified" => if(search_result.certified, do: search_result.certified, else: false)
} }
end end
def prepare_search_result(%{type: address_or_contract_or_label} = search_result) def prepare_search_result(%{type: address_or_contract_or_label} = search_result)
when address_or_contract_or_label in ["address", "contract", "label"] do when address_or_contract_or_label in ["address", "label"] do
%{ %{
"type" => search_result.type, "type" => search_result.type,
"name" => search_result.name, "name" => search_result.name,

@ -208,7 +208,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do
do: format_constructor_arguments(target_contract.abi, target_contract.constructor_arguments) do: format_constructor_arguments(target_contract.abi, target_contract.constructor_arguments)
), ),
"language" => smart_contract_language(smart_contract), "language" => smart_contract_language(smart_contract),
"license_type" => smart_contract.license_type "license_type" => smart_contract.license_type,
"certified" => if(smart_contract.certified, do: smart_contract.certified, else: false)
} }
|> Map.merge(bytecode_info(address)) |> Map.merge(bytecode_info(address))
end end
@ -326,7 +327,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do
"has_constructor_args" => !is_nil(smart_contract.constructor_arguments), "has_constructor_args" => !is_nil(smart_contract.constructor_arguments),
"coin_balance" => "coin_balance" =>
if(smart_contract.address.fetched_coin_balance, do: smart_contract.address.fetched_coin_balance.value), if(smart_contract.address.fetched_coin_balance, do: smart_contract.address.fetched_coin_balance.value),
"license_type" => smart_contract.license_type "license_type" => smart_contract.license_type,
"certified" => if(smart_contract.certified, do: smart_contract.certified, else: false)
} }
end end

@ -124,7 +124,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do
"is_verified_via_eth_bytecode_db" => target_contract.verified_via_eth_bytecode_db, "is_verified_via_eth_bytecode_db" => target_contract.verified_via_eth_bytecode_db,
"is_verified_via_verifier_alliance" => target_contract.verified_via_verifier_alliance, "is_verified_via_verifier_alliance" => target_contract.verified_via_verifier_alliance,
"language" => smart_contract_language(target_contract), "language" => smart_contract_language(target_contract),
"license_type" => "none" "license_type" => "none",
"certified" => false
} }
get_eip1967_implementation_non_zero_address() get_eip1967_implementation_non_zero_address()
@ -226,7 +227,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do
"is_verified_via_eth_bytecode_db" => target_contract.verified_via_eth_bytecode_db, "is_verified_via_eth_bytecode_db" => target_contract.verified_via_eth_bytecode_db,
"is_verified_via_verifier_alliance" => target_contract.verified_via_verifier_alliance, "is_verified_via_verifier_alliance" => target_contract.verified_via_verifier_alliance,
"language" => smart_contract_language(target_contract), "language" => smart_contract_language(target_contract),
"license_type" => "gnu_agpl_v3" "license_type" => "gnu_agpl_v3",
"certified" => false
} }
get_eip1967_implementation_error_response() get_eip1967_implementation_error_response()
@ -330,7 +332,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do
"is_verified_via_eth_bytecode_db" => target_contract.verified_via_eth_bytecode_db, "is_verified_via_eth_bytecode_db" => target_contract.verified_via_eth_bytecode_db,
"is_verified_via_verifier_alliance" => target_contract.verified_via_verifier_alliance, "is_verified_via_verifier_alliance" => target_contract.verified_via_verifier_alliance,
"language" => smart_contract_language(target_contract), "language" => smart_contract_language(target_contract),
"license_type" => "none" "license_type" => "none",
"certified" => false
} }
get_eip1967_implementation_error_response() get_eip1967_implementation_error_response()
@ -450,7 +453,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do
"is_verified_via_eth_bytecode_db" => implementation_contract.verified_via_eth_bytecode_db, "is_verified_via_eth_bytecode_db" => implementation_contract.verified_via_eth_bytecode_db,
"is_verified_via_verifier_alliance" => implementation_contract.verified_via_verifier_alliance, "is_verified_via_verifier_alliance" => implementation_contract.verified_via_verifier_alliance,
"language" => smart_contract_language(implementation_contract), "language" => smart_contract_language(implementation_contract),
"license_type" => "bsd_3_clause" "license_type" => "bsd_3_clause",
"certified" => false
} }
request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(proxy_address.hash)}") request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(proxy_address.hash)}")

@ -134,6 +134,8 @@ config :explorer, Explorer.Integrations.EctoLogger, query_time_ms_threshold: :ti
config :explorer, Explorer.Tags.AddressTag.Cataloger, enabled: true config :explorer, Explorer.Tags.AddressTag.Cataloger, enabled: true
config :explorer, Explorer.SmartContract.CertifiedSmartContractCataloger, enabled: true
config :explorer, Explorer.Repo, migration_timestamps: [type: :utc_datetime_usec] config :explorer, Explorer.Repo, migration_timestamps: [type: :utc_datetime_usec]
config :explorer, Explorer.Tracer, config :explorer, Explorer.Tracer,

@ -123,6 +123,7 @@ defmodule Explorer.Application do
configure(Explorer.Counters.Transactions24hStats), configure(Explorer.Counters.Transactions24hStats),
configure(Explorer.Validator.MetadataProcessor), configure(Explorer.Validator.MetadataProcessor),
configure(Explorer.Tags.AddressTag.Cataloger), configure(Explorer.Tags.AddressTag.Cataloger),
configure(Explorer.SmartContract.CertifiedSmartContractCataloger),
configure(MinMissingBlockNumber), configure(MinMissingBlockNumber),
configure(Explorer.Chain.Fetcher.CheckBytecodeMatchingOnDemand), configure(Explorer.Chain.Fetcher.CheckBytecodeMatchingOnDemand),
configure(Explorer.Chain.Fetcher.FetchValidatorInfoOnDemand), configure(Explorer.Chain.Fetcher.FetchValidatorInfoOnDemand),

@ -47,6 +47,7 @@ defmodule Explorer.Chain.Search do
from(items in subquery(query), from(items in subquery(query),
order_by: [ order_by: [
desc: items.priority, desc: items.priority,
desc_nulls_last: items.certified,
desc_nulls_last: items.circulating_market_cap, desc_nulls_last: items.circulating_market_cap,
desc_nulls_last: items.exchange_rate, desc_nulls_last: items.exchange_rate,
desc_nulls_last: items.is_verified_via_admin_panel, desc_nulls_last: items.is_verified_via_admin_panel,
@ -324,6 +325,7 @@ defmodule Explorer.Chain.Search do
|> Map.put(:icon_url, dynamic([token, _], token.icon_url)) |> Map.put(:icon_url, dynamic([token, _], token.icon_url))
|> Map.put(:token_type, dynamic([token, _], token.type)) |> Map.put(:token_type, dynamic([token, _], token.type))
|> Map.put(:verified, dynamic([_, smart_contract], not is_nil(smart_contract))) |> Map.put(:verified, dynamic([_, smart_contract], not is_nil(smart_contract)))
|> Map.put(:certified, dynamic([_, smart_contract], smart_contract.certified))
|> Map.put(:exchange_rate, dynamic([token, _], token.fiat_value)) |> Map.put(:exchange_rate, dynamic([token, _], token.fiat_value))
|> Map.put(:total_supply, dynamic([token, _], token.total_supply)) |> Map.put(:total_supply, dynamic([token, _], token.total_supply))
|> Map.put(:circulating_market_cap, dynamic([token, _], token.circulating_market_cap)) |> Map.put(:circulating_market_cap, dynamic([token, _], token.circulating_market_cap))
@ -355,6 +357,7 @@ defmodule Explorer.Chain.Search do
|> Map.put(:type, "contract") |> Map.put(:type, "contract")
|> Map.put(:name, dynamic([smart_contract, _], smart_contract.name)) |> Map.put(:name, dynamic([smart_contract, _], smart_contract.name))
|> Map.put(:inserted_at, dynamic([_, address], address.inserted_at)) |> Map.put(:inserted_at, dynamic([_, address], address.inserted_at))
|> Map.put(:certified, dynamic([smart_contract, _], smart_contract.certified))
|> Map.put(:verified, true) |> Map.put(:verified, true)
from(smart_contract in SmartContract, from(smart_contract in SmartContract,
@ -635,6 +638,7 @@ defmodule Explorer.Chain.Search do
token_type: nil, token_type: nil,
timestamp: dynamic([_, _], type(^nil, :utc_datetime_usec)), timestamp: dynamic([_, _], type(^nil, :utc_datetime_usec)),
verified: nil, verified: nil,
certified: nil,
exchange_rate: nil, exchange_rate: nil,
total_supply: nil, total_supply: nil,
circulating_market_cap: nil, circulating_market_cap: nil,

@ -277,6 +277,7 @@ defmodule Explorer.Chain.SmartContract do
* `implementation_address_hash` - address hash of the proxy's implementation if any * `implementation_address_hash` - address hash of the proxy's implementation if any
* `autodetect_constructor_args` - field was added for storing user's choice * `autodetect_constructor_args` - field was added for storing user's choice
* `is_yul` - 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.
""" """
typed_schema "smart_contracts" do typed_schema "smart_contracts" do
field(:name, :string, null: false) field(:name, :string, null: false)
@ -305,6 +306,7 @@ defmodule Explorer.Chain.SmartContract do
field(:is_yul, :boolean, virtual: true) field(:is_yul, :boolean, virtual: true)
field(:metadata_from_verified_twin, :boolean, virtual: true) field(:metadata_from_verified_twin, :boolean, virtual: true)
field(:license_type, Ecto.Enum, values: @license_enum, default: :none) field(:license_type, Ecto.Enum, values: @license_enum, default: :none)
field(:certified, :boolean)
has_many( has_many(
:decompiled_smart_contracts, :decompiled_smart_contracts,
@ -358,7 +360,8 @@ defmodule Explorer.Chain.SmartContract do
:compiler_settings, :compiler_settings,
:implementation_address_hash, :implementation_address_hash,
:implementation_fetched_at, :implementation_fetched_at,
:license_type :license_type,
:certified
]) ])
|> validate_required([ |> validate_required([
:name, :name,
@ -401,7 +404,8 @@ defmodule Explorer.Chain.SmartContract do
:contract_code_md5, :contract_code_md5,
:implementation_name, :implementation_name,
:autodetect_constructor_args, :autodetect_constructor_args,
:license_type :license_type,
:certified
]) ])
|> (&if(verification_with_files?, |> (&if(verification_with_files?,
do: &1, do: &1,
@ -1279,6 +1283,26 @@ defmodule Explorer.Chain.SmartContract do
end end
end end
@doc """
Sets smart-contract certified flag
"""
@spec set_smart_contracts_certified_flag(list()) ::
{:ok, []} | {:error, String.t()}
def set_smart_contracts_certified_flag([]), do: {:ok, []}
def set_smart_contracts_certified_flag(address_hashes) do
query =
from(
contract in __MODULE__,
where: contract.address_hash in ^address_hashes
)
case Repo.update_all(query, set: [certified: true]) do
{1, _} -> {:ok, []}
_ -> {:error, "There was an error in setting certified flag."}
end
end
defp check_verified_with_full_match(address_hash, options) do defp check_verified_with_full_match(address_hash, options) do
smart_contract = address_hash_to_smart_contract_without_twin(address_hash, options) smart_contract = address_hash_to_smart_contract_without_twin(address_hash, options)

@ -4,7 +4,7 @@ defmodule Explorer.CustomContractsHelper do
""" """
def get_custom_addresses_list(env_var) do def get_custom_addresses_list(env_var) do
addresses_var = get_raw_custom_addresses_list(env_var) addresses_var = Application.get_env(:block_scout_web, env_var)
addresses_list = (addresses_var && String.split(addresses_var, ",")) || [] addresses_list = (addresses_var && String.split(addresses_var, ",")) || []
formatted_addresses_list = formatted_addresses_list =
@ -15,8 +15,4 @@ defmodule Explorer.CustomContractsHelper do
formatted_addresses_list formatted_addresses_list
end end
def get_raw_custom_addresses_list(env_var) do
Application.get_env(:block_scout_web, env_var)
end
end end

@ -0,0 +1,29 @@
defmodule Explorer.SmartContract.CertifiedSmartContractCataloger do
@moduledoc """
Actualizes certified smart-contracts.
"""
use GenServer, restart: :transient
alias Explorer.Chain.SmartContract
def start_link(_) do
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
end
@impl GenServer
def init(args) do
send(self(), :fetch_certified_smart_contracts)
{:ok, args}
end
@impl GenServer
def handle_info(:fetch_certified_smart_contracts, state) do
certified_contracts_list = Application.get_env(:block_scout_web, :contract)[:certified_list]
SmartContract.set_smart_contracts_certified_flag(certified_contracts_list)
{:noreply, state}
end
end

@ -0,0 +1,13 @@
defmodule Explorer.Repo.Migrations.SmartContractsAddCertifiedFlag do
use Ecto.Migration
@disable_ddl_transaction true
@disable_migration_lock true
def change do
alter table("smart_contracts") do
add(:certified, :boolean, null: true)
end
create_if_not_exists(index(:smart_contracts, [:certified]))
end
end

@ -249,6 +249,25 @@ defmodule ConfigHelper do
err -> raise "Invalid JSON in environment variable #{env_var}: #{inspect(err)}" err -> raise "Invalid JSON in environment variable #{env_var}: #{inspect(err)}"
end end
@spec parse_list_env_var(String.t(), String.t() | nil) :: list()
def parse_list_env_var(env_var, default_value \\ nil) do
addresses_var = safe_get_env(env_var, default_value)
if addresses_var !== "" do
addresses_list = (addresses_var && String.split(addresses_var, ",")) || []
formatted_addresses_list =
addresses_list
|> Enum.map(fn addr ->
String.downcase(addr)
end)
formatted_addresses_list
else
[]
end
end
@supported_chain_types [ @supported_chain_types [
"default", "default",
"arbitrum", "arbitrum",

@ -86,7 +86,8 @@ config :block_scout_web, :footer,
config :block_scout_web, :contract, config :block_scout_web, :contract,
verification_max_libraries: ConfigHelper.parse_integer_env_var("CONTRACT_VERIFICATION_MAX_LIBRARIES", 10), verification_max_libraries: ConfigHelper.parse_integer_env_var("CONTRACT_VERIFICATION_MAX_LIBRARIES", 10),
max_length_to_show_string_without_trimming: System.get_env("CONTRACT_MAX_STRING_LENGTH_WITHOUT_TRIMMING", "2040"), max_length_to_show_string_without_trimming: System.get_env("CONTRACT_MAX_STRING_LENGTH_WITHOUT_TRIMMING", "2040"),
disable_interaction: ConfigHelper.parse_bool_env_var("CONTRACT_DISABLE_INTERACTION") disable_interaction: ConfigHelper.parse_bool_env_var("CONTRACT_DISABLE_INTERACTION"),
certified_list: ConfigHelper.parse_list_env_var("CONTRACT_CERTIFIED_LIST", "")
default_global_api_rate_limit = 50 default_global_api_rate_limit = 50
default_api_rate_limit_by_key = 10 default_api_rate_limit_by_key = 10

@ -109,6 +109,7 @@ CONTRACT_MAX_STRING_LENGTH_WITHOUT_TRIMMING=2040
# CONTRACT_DISABLE_INTERACTION= # CONTRACT_DISABLE_INTERACTION=
# CONTRACT_AUDIT_REPORTS_AIRTABLE_URL= # CONTRACT_AUDIT_REPORTS_AIRTABLE_URL=
# CONTRACT_AUDIT_REPORTS_AIRTABLE_API_KEY= # CONTRACT_AUDIT_REPORTS_AIRTABLE_API_KEY=
# CONTRACT_CERTIFIED_LIST=
UNCLES_IN_AVERAGE_BLOCK_TIME=false UNCLES_IN_AVERAGE_BLOCK_TIME=false
DISABLE_WEBAPP=false DISABLE_WEBAPP=false
API_V2_ENABLED=true API_V2_ENABLED=true

Loading…
Cancel
Save