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

Add Vyper support (WIP) #25

Merged
merged 7 commits into from
Jul 2, 2019
Merged
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
17 changes: 16 additions & 1 deletion crytic_compile/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,17 @@ def parse_args():
default='crytic.config.json')

parser.add_argument('--export-format',
help='Export json with non crytic-compile format (default None. Accepted: solc, truffle)',
help='Export json with non crytic-compile format (default None. Accepted: standard, solc, truffle)',
action='store',
dest='export_format',
default=None)

parser.add_argument('--export-formats',
help='Comma-separated list of export format, defaults to None',
action='store',
dest='export_formats',
default=None)

parser.add_argument('--export-dir',
help='Export directory (default: crytic-export)',
action='store',
Expand Down Expand Up @@ -101,10 +107,19 @@ def main():
print(f'\tShort: {filename.short}')
print(f'\tUsed: {filename.used}')
printed_filenames.add(unique_id)
if args.export_format:
compilation.export(**vars(args))

if args.export_formats:
for format in args.export_formats.split(','):
args.export_format = format
compilation.export(**vars(args))

if args.export_to_zip:
save_to_zip(compilations, args.export_to_zip)



except InvalidCompilation as e:
logger.error(e)
sys.exit(-1)
Expand Down
71 changes: 48 additions & 23 deletions crytic_compile/crytic_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import sha3
from pathlib import Path

from .platform import solc, truffle, embark, dapp, etherlime, etherscan, archive, standard
from .platform import solc, truffle, embark, dapp, etherlime, etherscan, archive, standard, vyper
from .utils.zip import load_from_zip

logger = logging.getLogger("CryticCompile")
Expand All @@ -23,7 +23,8 @@ def is_supported(target):
etherlime.is_etherlime,
etherscan.is_etherscan,
standard.is_standard,
archive.is_archive]
archive.is_archive,
vyper.is_vyper]
return any(f(target) for f in supported) or target.endswith('.zip')

PLATFORMS = {'solc': solc,
Expand All @@ -33,7 +34,8 @@ def is_supported(target):
'etherlime': etherlime,
'etherscan': etherscan,
'archive': archive,
'standard': standard}
'standard': standard,
'vyper': vyper}

def compile_all(target, **kwargs):
"""
Expand All @@ -59,7 +61,9 @@ def compile_all(target, **kwargs):
# We create a new glob to find solidity files at this path (in case this is a directory)
filenames = glob.glob(os.path.join(target, "*.sol"))
if not filenames:
filenames = globbed_targets
filenames = glob.glob(os.path.join(target, "*.vy"))
if not filenames:
filenames = globbed_targets

# We compile each file and add it to our compilations.
for filename in filenames:
Expand Down Expand Up @@ -434,12 +438,12 @@ def _library_name_lookup(self, lib_name, original_contract):
- __X__ following Solidity 0.4 format
- __$..$__ following Solidity 0.5 format
:param lib_name:
:return: contract name (None if not found)
:return: (contract name, pattern) (None if not found)
"""

for name in self.contracts_names:
if name == lib_name:
return name
return (name, name)

# Some platform use only the contract name
# Some use fimename:contract_name
Expand All @@ -450,46 +454,50 @@ def _library_name_lookup(self, lib_name, original_contract):
name_with_used_filename = name_with_used_filename[0:36]

# Solidity 0.4
if '__' + name + '_' * (38-len(name)) == lib_name:
return name
solidity_0_4 = '__' + name + '_' * (38-len(name))
if solidity_0_4 == lib_name:
return (name, solidity_0_4)

# Solidity 0.4 with filename
if '__' + name_with_absolute_filename+ '_' * (38-len(name_with_absolute_filename)) == lib_name:
return name
solidity_0_4_filename = '__' + name_with_absolute_filename+ '_' * (38-len(name_with_absolute_filename))
if solidity_0_4_filename == lib_name:
return (name, solidity_0_4_filename)

# Solidity 0.4 with filename
if '__' + name_with_used_filename+ '_' * (38-len(name_with_used_filename)) == lib_name:
return name
solidity_0_4_filename = '__' + name_with_used_filename + '_' * (38 - len(name_with_used_filename))
if solidity_0_4_filename == lib_name:
return (name, solidity_0_4_filename)


# Solidity 0.5
s = sha3.keccak_256()
s.update(name.encode('utf-8'))
v5_name = "__$" + s.hexdigest()[:34] + "$__"

if v5_name == lib_name:
return name
if v5_name == lib_name:
return (name, v5_name)

# Solidity 0.5 with filename
s = sha3.keccak_256()
s.update(name_with_absolute_filename .encode('utf-8'))
v5_name = "__$" + s.hexdigest()[:34] + "$__"

if v5_name == lib_name:
return name
if v5_name == lib_name:
return (name, v5_name)

s = sha3.keccak_256()
s.update(name_with_used_filename.encode('utf-8'))
v5_name = "__$" + s.hexdigest()[:34] + "$__"

if v5_name == lib_name:
return name
return (name, v5_name)

# handle specific case of colission for Solidity <0.4
# handle specific case of collision for Solidity <0.4
# We can only detect that the second contract is meant to be the library
# if there is only two contracts in the codebase
if len(self._contracts_name) == 2:
return next((c for c in self._contracts_name if c != original_contract), None)
return next(((c, '__' + c + '_' * (38-len(c))) for c in self._contracts_name if c != original_contract),
None)

return None

Expand All @@ -504,8 +512,20 @@ def libraries_names(self, name):
init = re.findall(r'__.{36}__', self.bytecode_init(name))
runtime = re.findall(r'__.{36}__', self.bytecode_runtime(name))
self._libraries[name] = [self._library_name_lookup(x, name) for x in set(init+runtime)]
return self._libraries[name]
return [name for (name, pattern) in self._libraries[name]]

def libraries_names_and_patterns(self, name):
"""
Return the name of the libraries used by the contract
:param name: contract
:return: list of (libraries name, pattern)
"""

if name not in self._libraries:
init = re.findall(r'__.{36}__', self.bytecode_init(name))
runtime = re.findall(r'__.{36}__', self.bytecode_runtime(name))
self._libraries[name] = [self._library_name_lookup(x, name) for x in set(init+runtime)]
return self._libraries[name]

def _update_bytecode_with_libraries(self, bytecode, libraries):
if libraries:
Expand Down Expand Up @@ -577,12 +597,13 @@ def export(self, **kwargs):
The json format can be crytic-compile, solc or truffle.
solc format is --combined-json bin-runtime,bin,srcmap,srcmap-runtime,abi,ast,compact-format
Keyword Args:
export_format (str): export format (default None). Accepted: None, 'solc', 'truffle', 'archive'
export_format (str): export format (default None). Accepted: None, 'solc', 'truffle',
'crytic-compile', 'standard'
export_dir (str): export dir (default crytic-export)
"""
export_format = kwargs.get('export_format', None)
if export_format is None or export_format == "crytic-compile":
return standard.export(**kwargs)
if export_format is None or export_format in ["crytic-compile", "standard"]:
return standard.export(self, **kwargs)
elif export_format == "solc":
return solc.export(self, **kwargs)
elif export_format == "truffle":
Expand Down Expand Up @@ -610,6 +631,7 @@ def _compile(self, target, **kwargs):
etherscan_ignore = kwargs.get('etherscan_ignore', False)
standard_ignore = kwargs.get('standard_ignore', False)
archive_ignore = kwargs.get('standard_ignore', False)
vyper_ignore = kwargs.get('vyper_ignore', False)

custom_build = kwargs.get('compile_custom_build', None)

Expand All @@ -621,6 +643,7 @@ def _compile(self, target, **kwargs):
etherscan_ignore = True
standard_ignore = True
archive_ignore = True
vyper_ignore = True

self._run_custom_build(custom_build)

Expand All @@ -642,6 +665,8 @@ def _compile(self, target, **kwargs):
self._platform = standard
elif not archive_ignore and archive.is_archive(target):
self._platform = archive
elif not vyper_ignore and vyper.is_vyper(target):
self._platform = vyper
# .json or .sol provided
else:
self._platform = solc
Expand Down
3 changes: 1 addition & 2 deletions crytic_compile/platform/solc.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,7 @@ def _run_solc(crytic_compile, filename, solc, solc_disable_warnings, solc_argume
:return:
"""
if not os.path.isfile(filename):
logger.error('{} does not exist (are you in the correct directory?)'.format(filename))
exit(-1)
raise InvalidCompilation('{} does not exist (are you in the correct directory?)'.format(filename))

if not filename.endswith('.sol'):
raise InvalidCompilation('Incorrect file format')
Expand Down
5 changes: 4 additions & 1 deletion crytic_compile/platform/standard.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def generate_standard_export(crytic_compile):
contracts = dict()
for contract_name in crytic_compile.contracts_names:
filename = crytic_compile.filename_of_contract(contract_name)
librairies = crytic_compile.libraries_names_and_patterns(contract_name)
contracts[contract_name] = {
'abi': crytic_compile.abi(contract_name),
'bin': crytic_compile.bytecode_init(contract_name),
Expand All @@ -25,6 +26,7 @@ def generate_standard_export(crytic_compile):
'short': filename.short,
'relative': filename.used
},
'libraries': dict(librairies) if librairies else dict(),
'is_dependency': crytic_compile._platform.is_dependency(filename.absolute)
}

Expand Down Expand Up @@ -55,7 +57,7 @@ def export(crytic_compile, **kwargs):
target = crytic_compile.target
target = "contracts" if os.path.isdir(target) else Path(target).parts[-1]

path = os.path.join(export_dir, f"{target}_archive.json")
path = os.path.join(export_dir, f"{target}.json")
with open(path, 'w', encoding='utf8') as f:
json.dump(output, f)

Expand Down Expand Up @@ -85,6 +87,7 @@ def load_from_compile(crytic_compile, loaded_json):
crytic_compile._runtime_bytecodes[contract_name] = contract['bin-runtime']
crytic_compile._srcmaps[contract_name] = contract['srcmap'].split(';')
crytic_compile._srcmaps_runtime[contract_name] = contract['srcmap-runtime'].split(';')
crytic_compile._libraries[contract_name] = contract['libraries']

if contract['is_dependency']:
crytic_compile._is_dependencies.add(filename.absolute)
Expand Down
3 changes: 3 additions & 0 deletions crytic_compile/platform/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class Type(IntEnum):
ETHERSCAN = 6
STANDARD = 7
ARCHIVE = 8
VYPER = 9

def __str__(self):
if self == Type.SOLC:
Expand All @@ -28,4 +29,6 @@ def __str__(self):
return 'Standard'
if self == Type.ARCHIVE:
return 'Archive'
if self == Type.VYPER:
return 'Archive'
raise ValueError
76 changes: 76 additions & 0 deletions crytic_compile/platform/vyper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import subprocess
import os
import re
from pathlib import Path
import json
from .types import Type

from ..compiler.compiler import CompilerVersion
from .exceptions import InvalidCompilation
from ..utils.naming import extract_filename, extract_name, combine_filename_name, convert_filename

def is_vyper(target):
return os.path.isfile(target) and target.endswith('.vy')

def compile(crytic_compile, target, **kwargs):

crytic_compile.type = Type.VYPER

vyper = kwargs.get('vyper', 'vyper')


targets_json = _run_vyper(target, vyper)

assert 'version' in targets_json
crytic_compile.compiler_version = CompilerVersion(compiler="vyper",
version=targets_json['version'],
optimized=False)

assert target in targets_json

info = targets_json[target]
contract_filename = convert_filename(target, _relative_to_short)


contract_name = Path(target).parts[-1]

crytic_compile.contracts_names.add(contract_name)
crytic_compile.contracts_filenames[contract_name] = contract_filename
crytic_compile.abis[contract_name] = info['abi']
crytic_compile.bytecodes_init[contract_name] = info['bytecode'].replace('0x', '')
crytic_compile.bytecodes_runtime[contract_name] = info['bytecode_runtime'].replace('0x', '')
crytic_compile.srcmaps_init[contract_name] = []
crytic_compile.srcmaps_runtime[contract_name] = []

crytic_compile.filenames.add(contract_filename)
#crytic_compile.asts[path.absolute] = info['AST']


def _run_vyper(filename, vyper, env=None, working_dir=None):
if not os.path.isfile(filename):
raise InvalidCompilation('{} does not exist (are you in the correct directory?)'.format(filename))

cmd = [vyper, filename, "-f", "combined_json"]

additional_kwargs = {'cwd': working_dir} if working_dir else {}
try:
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, **additional_kwargs)
except Exception as e:
raise InvalidCompilation(e)

stdout, stderr = process.communicate()

try:
res = stdout.split(b'\n')
res = res[-2]
return json.loads(res)

except json.decoder.JSONDecodeError:
raise InvalidCompilation(f'Invalid vyper compilation\n{stderr}')


def _relative_to_short(relative):
return relative

def is_dependency(_path):
return False