Skip to content

Commit

Permalink
feat: add contract verify on blockscout and etherscan
Browse files Browse the repository at this point in the history
  • Loading branch information
developerfred committed Sep 6, 2024
1 parent 436b6c3 commit c7aea18
Show file tree
Hide file tree
Showing 5 changed files with 260 additions and 10 deletions.
24 changes: 24 additions & 0 deletions .env.unsafe.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
# 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!

# Existing environment variables
MAINNET_ENDPOINT=xxx
SEPOLIA_ENDPOINT=xxx
SEPOLIA_PKEY=xxx

# New environment variables for contract verification

# 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
110 changes: 109 additions & 1 deletion boa/contracts/vyper/vyper_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,98 @@
# error detail where user possibly provided dev revert reason
DEV_REASON_ALLOWED = ("user raise", "user assert")

class ContractVerifier:
def __init__(self, address: str, bytecode: str, source_code: str, compiler_version: str):
self.address = address
self.bytecode = bytecode
self.source_code = source_code
self.compiler_version = compiler_version

def verify(self, explorer: str) -> bool:
if explorer.lower() == "blockscout":
return self._verify_blockscout()
elif explorer.lower() == "etherscan":
return self._verify_etherscan()
else:
raise ValueError(f"Unsupported explorer: {explorer}")

def _verify_blockscout(self) -> bool:
api_key = os.getenv('BLOCKSCOUT_API_KEY')
if not api_key:
raise ValueError("BLOCKSCOUT_API_KEY not set in environment variables")

url = os.getenv('BLOCKSCOUT_API_URL', 'https://blockscout.com/poa/core/api')

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

params = {
"module": "contract",
"action": "verifysourcecode",
"addressHash": self.address,
"contractSourceCode": json.dumps(standard_json_input),
"name": "VerifiedContract",
"compilerVersion": self.compiler_version,
"optimization": "true",
"apikey": api_key
}
response = requests.post(url, params=params)
result = response.json()
return result.get("status") == "1"

def _verify_etherscan(self) -> 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": self.source_code
}
},
"settings": {
"optimizer": {
"enabled": True
},
"outputSelection": {
"*": ["evm.bytecode", "evm.deployedBytecode", "abi"]
}
}
}

params = {
"module": "contract",
"action": "verifysourcecode",
"contractaddress": self.address,
"sourceCode": json.dumps(standard_json_input),
"codeformat": "solidity-standard-json-input",
"contractname": "contract.vy:VerifiedContract",
"compilerversion": self.compiler_version,
"optimizationUsed": "1",
"apikey": api_key
}
response = requests.post(url, params=params)
result = response.json()
return result.get("status") == "1"

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

Expand All @@ -85,10 +176,27 @@ def __init__(self, compiler_data, filename=None):
def __call__(self, *args, **kwargs):
return self.deploy(*args, **kwargs)

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

if verify:
if not explorer:
raise ValueError("Explorer is required for verification")
verifier = ContractVerifier(
contract.address,
contract.bytecode,
self.compiler_data.source_code,
f"v{vyper.__version__}"
)
verification_result = verifier.verify(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(
Expand Down
22 changes: 20 additions & 2 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, verify=verify, explorer=explorer, **kwargs, filename=filename)


def loads(
Expand All @@ -181,21 +181,39 @@ def loads(
name=None,
filename=None,
compiler_args=None,
verify: bool = False,
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)
if verify:
if not explorer:
raise ValueError("Explorer is required for verification")
verifier = ContractVerifier(
contract.address,
contract.bytecode,
source_code,
f"v{vyper.__version__}"
)
verification_result = verifier.verify(explorer)
if verification_result:
print(f"Contract verified successfully on {explorer}")
else:
print(f"Contract verification failed on {explorer}")
return contract



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
71 changes: 65 additions & 6 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ High-Level Functionality

The global environment object.

.. function:: load(fp: str, *args: Any, **kwargs: Any) -> VyperContract | VyperBlueprint
.. function:: load(fp: str, *args: Any, verify: bool = False, 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 verify: Whether to verify the contract on a block explorer after deployment.
: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 +40,22 @@ 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)}, verify=True, 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 verify: Whether to verify the contract on a block explorer after deployment.
: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 +69,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 @@ -689,6 +697,57 @@ Low-Level Functionality
.. property:: deployer
:type: VyperDeployer
.. class:: ContractVerifier

A utility class for verifying deployed contracts on block explorers.

.. method:: __init__(address: str, bytecode: str, source_code: str, compiler_version: str)

Initialize a ContractVerifier instance.

:param address: The address of the deployed contract.
:param bytecode: The bytecode of the deployed contract.
:param source_code: The source code of the contract.
:param compiler_version: The version of the Vyper compiler used.

.. method:: verify(explorer: str) -> bool

Verify the contract on the specified block explorer.

:param explorer: The block explorer to use for verification ("blockscout" or "etherscan").
:returns: True if verification was successful, False otherwise.

.. rubric:: Example

.. code-block:: python
>>> import boa
>>> src = """
... @external
... def main():
... pass
... """
>>> contract = boa.loads(src)
>>> verifier = boa.ContractVerifier(
... contract.address,
... contract.bytecode,
... src,
... f"v{boa.vyper.__version__}"
... )
>>> verification_result = verifier.verify("blockscout")
>>> print(verification_result)
True
.. 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
- `BLOCKSCOUT_API_URL` (optional): Custom API URL for Blockscout
- `ETHERSCAN_API_URL` (optional): Custom API URL for Etherscan

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

.. class:: VyperBlueprint

Expand Down
43 changes: 42 additions & 1 deletion tests/unitary/contracts/vyper/test_vyper_contract.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import boa

import os
import pytest
from unittest.mock import patch, MagicMock

def test_decode_struct():
code = """
Expand Down Expand Up @@ -72,3 +74,42 @@ def foo() -> bool:
c = boa.loads(code)

c.foo()


@pytest.mark.skipif(not os.getenv("BLOCKSCOUT_API_KEY"), reason="BLOCKSCOUT_API_KEY not set")
def test_contract_verification():
"""
This test case rigorously examines the contract verification process.
It deploys a rudimentary smart contract and subsequently attempts to verify it,
leveraging mock API responses to simulate both successful and failed verifications.
"""
code = """
@external
def hello() -> String[32]:
return "Hello, World!"
"""
# Fabricate a mock response emulating Blockscout's API
mock_response = MagicMock()
mock_response.json.return_value = {"status": "1"}

# Employ a context manager to intercept and replace the requests.post function
with patch('requests.post', return_value=mock_response):
# Instantiate and deploy the contract with immediate verification
contract = boa.loads(code, verify=True, explorer="blockscout")

# Ascertain the contract's successful deployment and correct type
assert isinstance(contract, boa.VyperContract), "Contract deployment failed or yielded unexpected type"

# Validate the contract's functionality post-deployment
assert contract.hello() == "Hello, World!", "Contract function 'hello()' produced unexpected output"

# Note: Verification success message should be logged or returned.
# Adapt the following assertion based on your implementation's specific output mechanism
# assert "Contract verified successfully" in captured_output

# Scrutinize the system's behavior when confronted with a failed verification scenario
mock_response.json.return_value = {"status": "0"}
with patch('requests.post', return_value=mock_response):
with pytest.raises(Exception) as excinfo:
boa.loads(code, verify=True, explorer="blockscout")
assert "Contract verification failed" in str(excinfo.value), "Expected exception not raised or incorrect error message"

0 comments on commit c7aea18

Please sign in to comment.