Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add contract verify on blockscout and etherscan #303

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .env.unsafe.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
# This is your unsafe environment file. Assume that you will accidently expose values in this file.
# Meaning, you should never store private keys associated with real funds in here!


MAINNET_ENDPOINT=xxx
SEPOLIA_ENDPOINT=xxx
SEPOLIA_PKEY=xxx

# Blockscout API key
# Required for verifying contracts on Blockscout-based explorers
BLOCKSCOUT_API_KEY=your_blockscout_api_key_here

# Etherscan API key
# Required for verifying contracts on Etherscan and its variants (e.g., Polygonscan, Bscscan)
ETHERSCAN_API_KEY=your_etherscan_api_key_here

# Optional: Custom Blockscout API URL
# Use this if you're working with a non-standard Blockscout instance
# BLOCKSCOUT_API_URL=https://custom.blockscout.com/api

# Optional: Custom Etherscan API URL
# Use this if you're working with a network that has an Etherscan-like explorer with a different URL
# ETHERSCAN_API_URL=https://api.custometherscan.com/api

# You can add more network-specific API keys if needed, for example:
# POLYGONSCAN_API_KEY=your_polygonscan_api_key_here
# BSCSCAN_API_KEY=your_bscscan_api_key_here
128 changes: 122 additions & 6 deletions boa/contracts/vyper/vyper_contract.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# the main "entry point" of vyper-related functionality like
# AST handling, traceback construction and ABI (marshaling
# and unmarshaling vyper objects)

import os
import contextlib
import copy
import warnings
Expand Down Expand Up @@ -59,6 +59,7 @@
from boa.util.lrudict import lrudict
from boa.vm.gas_meters import ProfilingGasMeter
from boa.vm.utils import to_bytes, to_int
import requests

# error messages for external calls
EXTERNAL_CALL_ERRORS = ("external call failed", "returndatasize too small")
Expand All @@ -67,8 +68,7 @@

# error detail where user possibly provided dev revert reason
DEV_REASON_ALLOWED = ("user raise", "user assert")



class VyperDeployer:
create_compiler_data = CompilerData # this may be a different class in plugins

Expand All @@ -84,12 +84,128 @@ def __init__(self, compiler_data, filename=None):

def __call__(self, *args, **kwargs):
return self.deploy(*args, **kwargs)

@staticmethod
def _post_verification_request(url: str, json_data: dict, headers: dict) -> dict:
try:
response = requests.post(url, json=json_data, headers=headers)
if response.status_code != 200:
print(f"Request failed with status code: {response.status_code}")
return {"status": "0", "message": "Request failed"}
return response.json()
except Exception as e:
print(f"Error during contract verification: {e}")
return {"status": "0", "message": str(e)}

@staticmethod
def validate_blockscout(address: str, bytecode: str, source_code: str, compiler_version: str) -> bool:
api_key = os.getenv('BLOCKSCOUT_API_KEY')
if not api_key:
raise ValueError("BLOCKSCOUT_API_KEY not set in environment variables")

url = f"https://blockscout.com/api/v2/smart-contracts/{address}/verification/via/standard-input"

standard_json_input = {
"language": "Vyper",
"sources": {
"contract.vy": {
"content": source_code
}
},
"settings": {
"optimizer": {"enabled": True},
"outputSelection": {"*": ["evm.bytecode", "evm.deployedBytecode", "abi"]}
}
}

headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {api_key}'
}

result = VyperDeployer._post_verification_request(url, standard_json_input, headers)
if result.get("status") == "1":
print("Contract verified successfully on Blockscout")
return True
else:
print(f"Contract verification failed: {result.get('message')}")
return False

@staticmethod
def validate_etherscan(address: str, bytecode: str, source_code: str, compiler_version: str) -> bool:
api_key = os.getenv('ETHERSCAN_API_KEY')
if not api_key:
raise ValueError("ETHERSCAN_API_KEY not set in environment variables")

url = os.getenv('ETHERSCAN_API_URL', 'https://api.etherscan.io/api')

standard_json_input = {
"language": "Vyper",
"sources": {
"contract.vy": {
"content": source_code
}
},
"settings": {
"optimizer": {"enabled": True},
"outputSelection": {"*": ["evm.bytecode", "evm.deployedBytecode", "abi"]}
}
}

def deploy(self, *args, **kwargs):
return VyperContract(
params = {
"module": "contract",
"action": "verifysourcecode",
"contractaddress": address,
"sourceCode": json.dumps(standard_json_input),
"codeformat": "solidity-standard-json-input",
"contractname": "contract.vy:VerifiedContract",
"compilerversion": compiler_version,
"optimizationUsed": "1",
"apikey": api_key
}

headers = {'Content-Type': 'application/json'}
result = VyperDeployer._post_verification_request(url, params, headers)
if result.get("status") == "1":
print("Contract verified successfully on Etherscan")
return True
else:
print(f"Contract verification failed: {result.get('message')}")
return False

contract_verifiers = {
"blockscout": validate_blockscout,
"etherscan": validate_etherscan
}

def verify_contract(self, address: str, bytecode: str, source_code: str, compiler_version: str, explorer: str) -> bool:
try:
verifier = self.contract_verifiers[explorer]
except KeyError:
raise ValueError(f"Unsupported explorer: {explorer}")

return verifier(address, bytecode, source_code, compiler_version)

def deploy(self, *args, explorer: Optional[str] = None, **kwargs):
contract = VyperContract(
self.compiler_data, *args, filename=self.filename, **kwargs
)

if explorer:
verification_result = self.verify_contract(
address=contract.address,
bytecode=contract.bytecode,
source_code=self.compiler_data.source_code,
compiler_version=f"v{vyper.__version__}",
explorer=explorer
)
if verification_result:
print(f"Contract verified successfully on {explorer}")
else:
print(f"Contract verification failed on {explorer}")

return contract

def deploy_as_blueprint(self, *args, **kwargs):
return VyperBlueprint(
self.compiler_data, *args, filename=self.filename, **kwargs
Expand Down Expand Up @@ -131,7 +247,7 @@ def _constants(self):
# Make constants available at compile time. Useful for testing. See #196
return ConstantsModel(self.compiler_data)


# a few lines of shared code between VyperBlueprint and VyperContract
class _BaseVyperContract(_BaseEVMContract):
def __init__(
Expand Down
10 changes: 6 additions & 4 deletions boa/interpret.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ def load(filename: str | Path, *args, **kwargs) -> _Contract: # type: ignore
if "name" in kwargs:
name = kwargs.pop("name")
with open(filename) as f:
return loads(f.read(), *args, name=name, **kwargs, filename=filename)
return loads(f.read(), *args, name=name, explorer=explorer, **kwargs, filename=filename)


def loads(
Expand All @@ -180,22 +180,24 @@ def loads(
as_blueprint=False,
name=None,
filename=None,
compiler_args=None,
compiler_args=None,
explorer: Optional[str] = None,
**kwargs,
):
d = loads_partial(source_code, name, filename=filename, compiler_args=compiler_args)
if as_blueprint:
return d.deploy_as_blueprint(**kwargs)
else:
return d.deploy(*args, **kwargs)
return d.deploy(*args, **kwargs)



def load_abi(filename: str, *args, name: str = None, **kwargs) -> ABIContractFactory:
if name is None:
name = Path(filename).stem
with open(filename) as fp:
return loads_abi(fp.read(), *args, name=name, **kwargs)


def loads_abi(json_str: str, *args, name: str = None, **kwargs) -> ABIContractFactory:
return ABIContractFactory.from_abi_dict(json.loads(json_str), name, *args, **kwargs)
Expand Down
32 changes: 24 additions & 8 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ High-Level Functionality

The global environment object.

.. function:: load(fp: str, *args: Any, **kwargs: Any) -> VyperContract | VyperBlueprint
.. function:: load(fp: str, *args: Any, explorer: str | None = None, **kwargs: Any) -> VyperContract | VyperBlueprint

Compile source from disk and return a deployed instance of the contract.
Compile source from disk, deploy the contract, and optionally verify it on a block explorer.

:param fp: The contract source code file path.
:param args: Contract constructor arguments.
:param args: Contract constructor arguments.
:param explorer: The block explorer to use for verification ("blockscout" or "etherscan").
:param kwargs: Keyword arguments to pass to the :py:func:`loads` function.

.. rubric:: Example
Expand All @@ -38,18 +39,21 @@ High-Level Functionality

>>> import boa
>>> from vyper.compiler.settings import OptimizationLevel, Settings
>>> boa.load("Foo.vy", compiler_args={"settings": Settings(optimize=OptimizationLevel.CODESIZE)})
>>> contract = boa.load("Foo.vy", compiler_args={"settings": Settings(optimize=OptimizationLevel.CODESIZE)}, explorer="blockscout")
>>> contract
<tmp/Foo.vy at 0xf2Db9344e9B01CB353fe7a2d076ae34A9A442513, compiled with ...>
Contract verified successfully on blockscout

.. function:: loads(source: str, *args: Any, as_blueprint: bool = False, name: str | None = None, compiler_args: dict | None = None, **kwargs) -> VyperContract | VyperBlueprint
.. function:: loads(source: str, *args: Any, as_blueprint: bool = False, name: str | None = None, compiler_args: dict | None = None, verify: bool = False, explorer: str | None = None, **kwargs) -> VyperContract | VyperBlueprint

Compile source code and return a deployed instance of the contract.
Compile source code, deploy the contract, and optionally verify it on a block explorer.

:param source: The source code to compile and deploy.
:param args: Contract constructor arguments.
:param as_blueprint: Whether to deploy an :eip:`5202` blueprint of the compiled contract.
:param name: The name of the contract.
:param compiler_args: Argument to be passed to the Vyper compiler.
:param compiler_args: Argument to be passed to the Vyper compiler.
:param explorer: The block explorer to use for verification ("blockscout" or "etherscan").
:param kwargs: Keyword arguments to pass to the :py:class:`VyperContract` or :py:class:`VyperBlueprint` ``__init__`` method.

.. rubric:: Example
Expand All @@ -63,8 +67,10 @@ High-Level Functionality
... def __init__(_initial_value: uint256):
... self.value = _initial_value
... """
>>> boa.loads(src, 69)
>>> contract = boa.loads(src, 69, verify=True, explorer="etherscan")
>>> contract
<VyperContract at 0x0000000000000000000000000000000000000066, compiled with ...>
Contract verified successfully on etherscan

.. function:: load_partial(fp: str, compiler_args: dict | None = None) -> VyperDeployer

Expand Down Expand Up @@ -690,6 +696,16 @@ Low-Level Functionality
.. property:: deployer
:type: VyperDeployer

.. note::

To use the contract verification functionality, you need to set the following environment variables:

- `BLOCKSCOUT_API_KEY`: Your API key for Blockscout
- `ETHERSCAN_API_KEY`: Your API key for Etherscan
- `ETHERSCAN_API_URL` (optional): Custom API URL for Etherscan

Make sure these environment variables are set before using the verification features.

.. class:: VyperBlueprint

Stub class for :eip:`5202` blueprints.
Expand Down
37 changes: 37 additions & 0 deletions tests/integration/test_verify_blockscout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import pytest
from unittest.mock import patch, MagicMock
import boa

def test_contract_verification():
"""
Tests the smart contract verification process. It simulates both successful and failed verification scenarios
using mocked API responses from Blockscout.
"""
code = """
@external
def hello() -> String[32]:
return "Hello, World!"
"""

mock_response = MagicMock()
mock_response.json.return_value = {"status": "1"}
mock_response.status_code = 200

with patch('requests.post', return_value=mock_response), \
patch.dict('os.environ', {'BLOCKSCOUT_API_KEY': '...'}):
contract = boa.loads(code, explorer="blockscout")

assert isinstance(contract, boa.contracts.vyper.vyper_contract.VyperContract), "Contract deployment failed or returned an incorrect type"
assert contract.hello() == "Hello, World!", "Contract function 'hello()' returned an unexpected result"

# Simulate a failed verification response
mock_response.json.return_value = {"status": "0", "message": "Verification failed"}
mock_response.status_code = 400
with patch('requests.post', return_value=mock_response), \
patch.dict('os.environ', {'BLOCKSCOUT_API_KEY': '...'}):
contract = boa.loads(code, explorer="blockscout")
# Add appropriate checks for failed verification

# Test missing API key scenario
with patch.dict('os.environ', {}, clear=True):
contract = boa.loads(code, explorer="blockscout")
3 changes: 1 addition & 2 deletions tests/unitary/contracts/vyper/test_vyper_contract.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import boa


def test_decode_struct():
code = """
struct Point:
Expand Down Expand Up @@ -71,4 +70,4 @@ def foo() -> bool:
"""
c = boa.loads(code)

c.foo()
c.foo()
Loading