Skip to content

Commit

Permalink
Added EtherScan contract verification.
Browse files Browse the repository at this point in the history
  • Loading branch information
miohtama committed Apr 4, 2017
1 parent de8593e commit 65ed64a
Show file tree
Hide file tree
Showing 10 changed files with 183 additions and 27 deletions.
2 changes: 2 additions & 0 deletions docs/source/commands.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.. _command-line:

=====================
Command line commands
=====================
Expand Down
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ This project aims to provide standard, secure smart contracts and tools to creat
install
commands
interact
verification
test
designchoices
other
Expand Down
Binary file added docs/source/screenshots/etherscan_verify.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 31 additions & 0 deletions docs/source/verification.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
=================================
Contract source code verification
=================================

.. contents:: :local:

Verifying contracts on EtherScan
================================

ICO package has a semi-automated process to verify deployed contracts on `EtherScan verification service <https://etherscan.io/verifyContract>`_.

Benefits of verification
========================

* You can see the state of your contract variables real time on EtherScan block explorer

* You prove that there are deterministic and verifiable builds for your deployed smart contracts

How verification works
======================

* You need to have Firefox installed with necessary Selenium drivers

* Give `--verify` option to a :ref:`deployment script <command-line>`

* After the command line script has deployed the contract a browser will open

* The script autofills the verification page details (source code, construction arguments, linked libraries)

.. image:: screenshots/etherscan_verify.png
:width: 600
42 changes: 34 additions & 8 deletions ico/deploypresale.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from populus.utils.cli import request_account_unlock

from ico.utils import get_constructor_arguments
from ico.utils import get_libraries
from ico.etherscan import verify_contract


def utc_time():
Expand All @@ -26,19 +28,27 @@ def utc_time():

@click.command()
@click.option('--chain', nargs=1, default="mainnet", help='On which chain to deploy - see populus.json')
@click.option('--address', nargs=1, help='Account to deploy from (must exist on geth)', required=True)
@click.option('--address', nargs=1, help='Account to deploy from. Must exist on geth.', required=True)
@click.option('--owner', nargs=1, help='Address that is set as owner of the presale contract', required=True)
@click.option('--days', nargs=1, default=30, help='How many days presale is frozen for', type=int)
@click.option('--minimum', nargs=1, default=1, help='What is the minimum pre-ico buy in (ether)', type=float)
@click.option('--verify/--no-verify', default=True, help='Verify contract source code one EtherScan.io')
def main(chain, address, days, minimum, verify):
"""Deploy a PresaleFundCollector contract."""
@click.option('--verify/--no-verify', default=False, help='Verify contract source code one EtherScan.io')
def main(chain, address, owner, days, minimum, verify):
"""Deploy a PresaleFundCollector contract.
Example:
deploy-presale --chain=ropsten --address=0x3c2d4e5eae8c4a31ccc56075b5fd81307b1627c6 --owner=0x3c2d4e5eae8c4a31ccc56075b5fd81307b1627c6 --days=30 --minimum=30
"""
project = Project()

# Parse command line args to presale constructor args
minimum = to_wei(minimum, "ether")
freeze_ends_at = int(utc_time() + days * 24*3600)

contract_name = "PresaleFundCollector"

# This is configured in populus.json
# We are working on a testnet
print("Make sure {} chain is running, you can connect to it, or you'll get timeout".format(chain))
Expand All @@ -58,17 +68,33 @@ def main(chain, address, days, minimum, verify):
request_account_unlock(c, address, None)

transaction = {"from": address}
args = [freeze_ends_at, minimum]
args = [owner, freeze_ends_at, minimum]

# This does deployment with all dependencies linked in
presale, txhash = c.provider.deploy_contract('PresaleFundCollector', deploy_transaction=transaction, deploy_args=args)
print("Deploying contracts")
presale, txhash = c.provider.deploy_contract(contract_name, deploy_transaction=transaction, deploy_args=args)
print("Deploying presale, tx hash is", txhash)
print("Presale contract address is", presale.address)

libraries = get_libraries(c, contract_name, presale)
print("Linked libraries are", libraries)

# This is needed for Etherscan contract verification
# https://etherscanio.freshdesk.com/support/solutions/articles/16000053599-contract-verification-constructor-arguments
data = get_constructor_arguments(presale, args)
print("Presale constructor arguments is", data)
constructor_args = get_constructor_arguments(presale, args)
print("Presale constructor arguments is", constructor_args)

if verify:
print("Verifying contract")
verify_contract(
project=project,
chain_name=chain,
address=presale.address,
contract_name="PresaleFundCollector",
contract_filename="PresaleFundCollector.sol",
constructor_args=constructor_args,
libraries=libraries)


# Do some contract reads to see everything looks ok
print("Presale freeze ends at", presale.call().freezeEndsAt())
Expand Down
60 changes: 42 additions & 18 deletions ico/etherscan.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
"""etherscan.io utilities."""

import requests
import time

from populus import Project

from ico.importexpand import expand_contract_imports


def verify_contract(project: Project, chain_name: str, address: str, contract_name, contract_filename: str, constructor_args: str, libraries: dict, optimization=True, compiler: str="v0.4.8-nightly.2017.1.13+commit.bde0b406"):
"""Make a contract verified on Etherscan.
def verify_contract(project: Project, chain_name: str, address: str, contract_name, contract_filename: str, constructor_args: str, libraries: dict, optimization=True, compiler: str="v0.4.8+commit.60cc1668"):
"""Semi-automated contract verified on Etherscan.
Uses a web browser + Selenium auto fill to verify contracts.
See the page in action: https://etherscan.io/verifyContract?a=0xcd111aa492a9c77a367c36e6d6af8e6f212e0c8e
"""

src = expand_contract_imports(project, contract_filename)
try:
from splinter import Browser
except ImportError:
raise RuntimeError("Splinter package must be installed for verification automation")

src, imported_files = expand_contract_imports(project, contract_filename)

if chain_name == "mainnet":
url = "https://etherscan.io/verifyContract"
Expand All @@ -22,20 +29,37 @@ def verify_contract(project: Project, chain_name: str, address: str, contract_na
else:
raise RuntimeError("Unknown chain")

data = {
"ctl00$ContentPlaceHolder1$txtContractAddress": address,
"ctl00$ContentPlaceHolder1$txtContractName": contract_name,
"ctl00$ContentPlaceHolder1$ddlCompilerVersions": compiler,
"ctl00$ContentPlaceHolder1$ddlOptimization": "1" if optimization else "0",
"ctl00$ContentPlaceHolder1$txtSourceCode": src,
"ctl00$ContentPlaceHolder1$txtConstructorArguements": constructor_args,
}
with Browser() as browser:
browser.visit(url)

browser.fill("ctl00$ContentPlaceHolder1$txtContractAddress", address)
browser.fill("ctl00$ContentPlaceHolder1$txtContractName", contract_name)
browser.select("ctl00$ContentPlaceHolder1$ddlCompilerVersions", compiler)
browser.select("ctl00$ContentPlaceHolder1$ddlOptimization", "1" if optimization else "0")
#browser.fill("ctl00$ContentPlaceHolder1$txtSourceCode", src)
browser.find_by_name("ctl00$ContentPlaceHolder1$txtSourceCode").first.value = src
browser.fill("ctl00$ContentPlaceHolder1$txtConstructorArguements", constructor_args)

idx = 1
for library_name, library_address in libraries.items():
browser.fill("ctl00$ContentPlaceHolder1$txtLibraryAddress{}".format(idx),library_address)
browser.fill("ctl00$ContentPlaceHolder1$txtLibraryName{}".format(idx), library_name)
idx += 1

browser.find_by_name("ctl00$ContentPlaceHolder1$btnSubmit").click()

deadline = time.time() + 60

print("Waiting EtherScan to process the contract verification")
while time.time() < deadline:
if browser.is_text_present("Successfully generated ByteCode and ABI for Contract Address", wait_time=1):
return

idx = 1
for library_name, library_address in libraries.items():
data["ctl00$ContentPlaceHolder1$txtLibraryAddress{}".format(idx)] = library_address
data["ctl00$ContentPlaceHolder1$txtLibraryName{}".format(idx)] = library_name
if browser.is_text_present("already been verified"):
print("The contract has already been verified")
return

resp = requests.post(url, data)
resp.raise_for_status()
time.sleep(1.0)

print("Contract verification failed. Check the browser for details.")
input("Press enter to continue")
8 changes: 7 additions & 1 deletion ico/importexpand.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class Expander:
def __init__(self, project: Project):
self.project = project
self.processed_imports = set()
self.pragma_processed = False

def expand_file(self, import_path: str):
"""Read Solidity source code and expart any import paths inside.
Expand Down Expand Up @@ -42,7 +43,6 @@ def expand_file(self, import_path: str):
self.processed_imports.add(import_path)
return self.process_source(source)


def process_source(self, src: str):
"""Process Solidity source code and expand any import statement."""

Expand All @@ -54,6 +54,12 @@ def process_source(self, src: str):
prefix, import_path, suffix = line.split('"')
source = self.expand_file(import_path)
out += source.split("\n")
elif line.startswith('pragma'):
# Only allow one pragma statement per file
if self.pragma_processed:
continue
else:
self.pragma_processed = True
else:
out.append(line)

Expand Down
27 changes: 27 additions & 0 deletions ico/tests/tools/manual_etherscan.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from ico.etherscan import verify_contract
from populus import Project


def manual_etherscan():
"""Manual test verification on EtherScan.io."""

contract_name = "PresaleFundCollector"
address = "0xb589ef3af084cc5ec905d23112520ec168478582"
constructor_args = "000000000000000000000000e8baf9df0ded92c5f28aab97f13936e7716a4a5b00000000000000000000000000000000000000000000000000000000590ba32f000000000000000000000000000000000000000000000002b5e3af16b1880000"
libraries = {'SafeMathLib': '0x8fd011ad5d39da2f0a09c2d89e7d6ae861fe42ba'}

p = Project()
chain_name = "mainnet"

verify_contract(
project=p,
chain_name=chain_name,
address=address,
contract_name="PresaleFundCollector",
contract_filename="PresaleFundCollector.sol",
constructor_args=constructor_args,
libraries=libraries)


if __name__ == "__main__":
manual_etherscan()
13 changes: 13 additions & 0 deletions ico/tests/tools/test_libraries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Library extraction from deployed contracts."""
from populus.chain import TestRPCChain
from web3.contract import Contract

from ico.utils import get_libraries


def test_extract_libraries(chain: TestRPCChain, uncapped_flatprice: Contract):
"""We get library information of deployed contract."""

libraries = get_libraries(chain, "UncappedCrowdsale", uncapped_flatprice)
assert libraries["SafeMathLib"].startswith("0x")

26 changes: 26 additions & 0 deletions ico/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
from web3.utils.abi import get_constructor_abi
from web3.utils.transactions import wait_for_transaction_receipt

from populus.chain.base import BaseChain
from populus.contracts.provider import Provider
from populus.utils.linking import find_link_references


def check_succesful_tx(web3: Web3, txid: str, timeout=180) -> dict:
"""See if transaction went through (Solidity code did not throw).
Expand All @@ -26,3 +30,25 @@ def get_constructor_arguments(contract: Contract, args: list):
"""
constructor_abi = get_constructor_abi(contract.abi)
return contract._encode_abi(constructor_abi, args)[2:] # No 0x


def get_libraries(chain: BaseChain, contract_name, contract: Contract) -> dict:
"""Get libraries of a deployed contract.
TODO: drop contract_name https://github.com/pipermerriam/web3.py/issues/172
:return dict: Library name -> address pairs
"""

unlinked = chain.provider.get_base_contract_factory(contract_name)
refs = find_link_references(unlinked.bytecode, chain.provider.get_all_contract_names())

def get_address(name):
return chain.registrar.get_contract_addresses(name)[0]

libraries = {
contract_name: get_address(contract_name)
for contract_name in set(ref.full_name for ref in refs)
}
return libraries

0 comments on commit 65ed64a

Please sign in to comment.