Skip to content
This repository was archived by the owner on Feb 14, 2025. It is now read-only.

Commit 0ce12c5

Browse files
authored
Merge pull request #61 from crytic/echidna-dev-etheno
Preliminary support for the Echidna refactor
2 parents 2c5642d + 5c3ccb5 commit 0ce12c5

File tree

7 files changed

+91
-50
lines changed

7 files changed

+91
-50
lines changed

CHANGELOG.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,26 @@
22

33
The format is based on [Keep a Changelog](http://keepachangelog.com/).
44

5-
## [Unreleased](https://github.com/trailofbits/etheno/compare/v0.2.0...HEAD)
5+
## [Unreleased](https://github.com/trailofbits/etheno/compare/v0.2.2...HEAD)
66

7-
### 0.2.1 — 2019-02-07
7+
## 0.2.2 — 2019-04-11
8+
9+
### Added
10+
11+
- Updated to support a [newer version of Echidna](https://github.com/crytic/echidna/tree/dev-etheno)
12+
- We are almost at feature parity with Echidna master, which we expect to happen at the next release
13+
- Two new commandline options to export raw transactions as a JSON file
14+
- New `--truffle-cmd` argument to specify the build command
15+
16+
### Changed
17+
18+
- The [`BrokenMetaCoin` example](examples/BrokenMetaCoin) was updated to a newer version of Solidity
19+
20+
### Fixed
21+
22+
- Fixes a bug in honoring the `--ganache-args` option
23+
24+
## 0.2.1 — 2019-02-07
825

926
Bugfix release.
1027

Dockerfile

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,33 @@ ENV LANG C.UTF-8
2929
# BEGIN Install Echidna
3030

3131
USER root
32-
RUN apt-get install -y libgmp-dev libbz2-dev libreadline-dev curl libsecp256k1-dev
32+
RUN apt-get install -y libgmp-dev libbz2-dev libreadline-dev curl libsecp256k1-dev software-properties-common locales-all locales zlib1g-dev
3333
RUN curl -sSL https://get.haskellstack.org/ | sh
3434
USER etheno
3535
RUN git clone https://github.com/trailofbits/echidna.git
3636
WORKDIR /home/etheno/echidna
37-
# Etheno currently requires the dev-no-hedgehog branch;
38-
RUN git checkout dev-no-hedgehog
37+
# Etheno currently requires the dev-etheno branch;
38+
RUN git checkout dev-etheno
3939
RUN stack upgrade
4040
RUN stack setup
4141
RUN stack install
4242
WORKDIR /home/etheno
4343

4444
# END Install Echidna
4545

46+
USER root
47+
48+
# Install Parity
49+
RUN apt-get install -y cmake libudev-dev
50+
RUN curl https://get.parity.io -L | bash
51+
52+
# Allow passwordless sudo for etheno
53+
RUN echo 'etheno ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers
54+
55+
RUN chown -R etheno:etheno /home/etheno/
56+
57+
USER etheno
58+
4659
RUN mkdir -p /home/etheno/etheno/etheno
4760

4861
COPY LICENSE /home/etheno/etheno
@@ -57,14 +70,8 @@ RUN cd etheno && pip3 install --user '.[manticore]'
5770

5871
USER root
5972

60-
# Install Parity
61-
RUN apt-get install -y cmake libudev-dev
62-
RUN curl https://get.parity.io -L | bash
63-
64-
# Allow passwordless sudo for etheno
65-
RUN echo 'etheno ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers
66-
67-
RUN chown -R etheno:etheno /home/etheno/
73+
RUN chown -R etheno:etheno /home/etheno/etheno
74+
RUN chown -R etheno:etheno /home/etheno/examples
6875

6976
USER etheno
7077

etheno/echidna.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import tempfile
44

55
from .ascii_escapes import decode
6-
from .client import JSONRPCError
76
from .etheno import EthenoPlugin
87
from .utils import ConstantTemporaryFile, format_hex_address
98

@@ -28,17 +27,21 @@
2827
}
2928
'''
3029

31-
ECHIDNA_CONFIG = b'''outputRawTxs: True\ngasLimit: 0xfffff\n'''
30+
ECHIDNA_CONFIG = b'''outputRawTxs: true\nquiet: true\ndashboard: false\ngasLimit: 0xfffff\n'''
31+
3232

3333
def echidna_exists():
3434
return subprocess.call(['/usr/bin/env', 'echidna-test', '--help'], stdout=subprocess.DEVNULL) == 0
3535

36+
3637
def stack_exists():
3738
return subprocess.call(['/usr/bin/env', 'stack', '--help'], stdout=subprocess.DEVNULL) == 0
3839

40+
3941
def git_exists():
4042
return subprocess.call(['/usr/bin/env', 'git', '--version'], stdout=subprocess.DEVNULL) == 0
4143

44+
4245
def install_echidna(allow_reinstall = False):
4346
if not allow_reinstall and echidna_exists():
4447
return
@@ -49,10 +52,11 @@ def install_echidna(allow_reinstall = False):
4952

5053
with tempfile.TemporaryDirectory() as path:
5154
subprocess.check_call(['/usr/bin/env', 'git', 'clone', 'https://github.com/trailofbits/echidna.git', path])
52-
# TODO: Once the `dev-no-hedgehog` branch is merged into `master`, we can remove this:
53-
subprocess.call(['/usr/bin/env', 'git', 'checkout', 'dev-no-hedgehog'], cwd=path)
55+
# TODO: Once the `dev-etheno` branch is merged into `master`, we can remove this:
56+
subprocess.call(['/usr/bin/env', 'git', 'checkout', 'dev-etheno'], cwd=path)
5457
subprocess.check_call(['/usr/bin/env', 'stack', 'install'], cwd=path)
55-
58+
59+
5660
def decode_binary_json(text):
5761
orig = text
5862
text = decode(text).strip()
@@ -73,6 +77,7 @@ def decode_binary_json(text):
7377
raise ValueError("Malformed JSON list! Expected '%s' but instead got '%s' at offset %d" % ('"', chr(text[-1]), offset + len(text) - 1))
7478
return text[:-1]
7579

80+
7681
class EchidnaPlugin(EthenoPlugin):
7782
def __init__(self, transaction_limit=None, contract_source=None):
7883
self._transaction = 0
@@ -93,6 +98,9 @@ def run(self):
9398
self.logger.info("Etheno does not know about any accounts, so Echidna has nothing to do!")
9499
self._shutdown()
95100
return
101+
elif self.contract_source is None:
102+
self.logger.error("Error compiling source contract")
103+
self._shutdown()
96104
# First, deploy the testing contract:
97105
self.logger.info('Deploying Echidna test contract...')
98106
self.contract_address = format_hex_address(self.etheno.deploy_contract(self.etheno.accounts[0], self.contract_bytecode), True)
@@ -172,5 +180,6 @@ def emit_transaction(self, txn):
172180
self.logger.info("Emitting Transaction %d" % self._transaction)
173181
self.etheno.post(transaction)
174182

183+
175184
if __name__ == '__main__':
176-
install_echidna(allow_reinstall = True)
185+
install_echidna(allow_reinstall=True)

etheno/etheno.py

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -57,54 +57,55 @@ def etheno(self, instance):
5757

5858
@property
5959
def log_directory(self):
60-
'''Returns a log directory that this client can use to save additional files, or None if one is not available'''
60+
"""Returns a log directory that this client can use to save additional files, or None if one is not available"""
6161
if self.logger is None:
6262
return None
6363
else:
6464
return self.logger.directory
6565

6666
def added(self):
67-
'''
67+
"""
6868
A callback when this plugin is added to an Etheno instance
69-
'''
69+
"""
7070
pass
7171

7272
def before_post(self, post_data):
73-
'''
73+
"""
7474
A callback when Etheno receives a JSON RPC POST, but before it is processed.
7575
:param post_data: The raw JSON RPC data
7676
:return: the post_data to be used by Etheno (can be modified)
77-
'''
77+
"""
7878
pass
7979

8080
def after_post(self, post_data, client_results):
81-
'''
81+
"""
8282
A callback when Etheno receives a JSON RPC POST after it is processed by all clients.
8383
:param post_data: The raw JSON RPC data
8484
:param client_results: A lost of the results returned by each client
85-
'''
85+
"""
8686
pass
8787

8888
def run(self):
89-
'''
89+
"""
9090
A callback when Etheno is running and all other clients and plugins are initialized
91-
'''
91+
"""
9292
pass
9393

9494
def finalize(self):
95-
'''
95+
"""
9696
Called when an analysis pass should be finalized (e.g., after a Truffle migration completes).
9797
Subclasses implementing this function should support it to be called multiple times in a row.
98-
'''
98+
"""
9999
pass
100100

101101
def shutdown(self):
102-
'''
102+
"""
103103
Called before Etheno shuts down.
104104
The default implementation calls `finalize()`.
105-
'''
105+
"""
106106
self.finalize()
107107

108+
108109
class Etheno(object):
109110
def __init__(self, master_client=None):
110111
self.accounts = []
@@ -151,11 +152,11 @@ def master_client(self, client):
151152
self._create_accounts(client)
152153

153154
def estimate_gas(self, transaction):
154-
'''
155+
"""
155156
Estimates the gas cost of a transaction.
156157
Iterates through all clients until it finds a client that is capable of estimating the gas cost without error.
157158
If all clients return an error, this function will return None.
158-
'''
159+
"""
159160
clients = [self.master_client] + self.clients
160161
for client in clients:
161162
try:
@@ -261,7 +262,7 @@ def add_client(self, client):
261262
self.clients.append(client)
262263
self._create_accounts(client)
263264

264-
def deploy_contract(self, from_address, bytecode, gas = 0x99999, gas_price = None, value = 0):
265+
def deploy_contract(self, from_address, bytecode, gas=0x99999, gas_price=None, value=0):
265266
if gas_price is None:
266267
gas_price = self.master_client.get_gas_price()
267268
if isinstance(bytecode, bytes):

etheno/genesis.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,22 @@
22

33
from .utils import format_hex_address
44

5+
56
class Account(object):
67
def __init__(self, address, balance = None, private_key = None):
78
self._address = address
89
self.balance = balance
910
self._private_key = private_key
11+
1012
@property
1113
def address(self):
1214
return self._address
15+
1316
@property
1417
def private_key(self):
1518
return self._private_key
1619

20+
1721
def make_genesis(network_id=0x657468656E6F, difficulty=20, gas_limit=200000000000, accounts=None, byzantium_block=0, dao_fork_block=0, homestead_block=0, eip150_block=0, eip155_block=0, eip158_block=0, constantinople_block=None):
1822
if accounts:
1923
alloc = {format_hex_address(acct.address): {'balance': "%d" % acct.balance, 'privateKey': format_hex_address(acct.private_key)} for acct in accounts}
@@ -40,8 +44,9 @@ def make_genesis(network_id=0x657468656E6F, difficulty=20, gas_limit=20000000000
4044

4145
return ret
4246

47+
4348
def geth_to_parity(genesis):
44-
'''Converts a Geth style genesis to Parity style'''
49+
"""Converts a Geth style genesis to Parity style"""
4550
ret = {
4651
'name': 'etheno',
4752
'engine': {
@@ -59,16 +64,17 @@ def geth_to_parity(genesis):
5964
# }
6065
},
6166
'genesis': {
62-
"seal": { "generic": "0x0"
63-
#'ethereum': {
64-
# 'nonce': '0x0000000000000042',
65-
# 'mixHash': '0x0000000000000000000000000000000000000000000000000000000000000000'
66-
#}
67-
},
68-
'difficulty': "0x%s" % genesis['difficulty'],
69-
'gasLimit': "0x%s" % genesis['gasLimit'],
67+
"seal": {
68+
"generic": "0x0"
69+
# 'ethereum': {
70+
# 'nonce': '0x0000000000000042',
71+
# 'mixHash': '0x0000000000000000000000000000000000000000000000000000000000000000'
72+
# }
73+
},
74+
'difficulty': "0x%s" % genesis['difficulty'],
75+
'gasLimit': "0x%s" % genesis['gasLimit'],
7076
'author': list(genesis['alloc'])[-1]
71-
},
77+
},
7278
'params': {
7379
'networkID' : "0x%x" % genesis['config']['chainId'],
7480
'maximumExtraDataSize': '0x20',
@@ -80,7 +86,7 @@ def geth_to_parity(genesis):
8086
'eip161dTransition': '0x0',
8187
'eip155Transition': "0x%x" % genesis['config']['eip155Block'],
8288
'eip98Transition': '0x7fffffffffffff',
83-
'eip86Transition': '0x7fffffffffffff',
89+
# 'eip86Transition': '0x7fffffffffffff',
8490
'maxCodeSize': 24576,
8591
'maxCodeSizeTransition': '0x0',
8692
'eip140Transition': '0x0',
@@ -100,9 +106,10 @@ def geth_to_parity(genesis):
100106

101107
return ret
102108

109+
103110
def make_accounts(num_accounts, default_balance = None):
104111
ret = []
105112
for i in range(num_accounts):
106113
acct = w3.eth.account.create()
107-
ret.append(Account(address = int(acct.address, 16), private_key = int(acct.privateKey.hex(), 16), balance = default_balance))
114+
ret.append(Account(address=int(acct.address, 16), private_key=int(acct.privateKey.hex(), 16), balance=default_balance))
108115
return ret

examples/ConstantinopleGasUsage/constantinople.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
pragma solidity ^0.4.24;
1+
pragma solidity ^0.5.4;
22
contract C {
33
int public stored = 1337;
44
function setStored(int value) public {
@@ -7,7 +7,7 @@ contract C {
77
function increment() public {
88
int newValue = stored + 1;
99
stored = 0;
10-
address(this).call(bytes4(keccak256("setStored(int256)")), newValue);
10+
address(this).call(abi.encodeWithSignature("setStored(int256)", newValue));
1111
}
1212
function echidna_() public returns (bool) {
1313
return true;

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
description='Etheno is a JSON RPC multiplexer, Manticore wrapper, differential fuzzer, and test framework integration tool.',
77
url='https://github.com/trailofbits/etheno',
88
author='Trail of Bits',
9-
version='0.2.1',
9+
version='0.2.2',
1010
packages=find_packages(),
1111
python_requires='>=3.6',
1212
install_requires=[

0 commit comments

Comments
 (0)