Fast assembler, disassembler, execution, and testing for the Ethereum Virtual Machine (EVM) supporting expressive syntax.
libcurl is required to build dio.
- macOS: already installed by default
- Arch Linux:
sudo pacman -S curl - Debian/Ubuntu:
sudo apt-get install libcurl4-openssl-dev - RHEL/CentOS/Fedora:
sudo dnf install libcurl-devel
git clone https://github.com/wjmelements/evm.git
cd evm
make bin/evm bin/dio
# Append bin/ to your $PATH
export PATH=$PATH:$pwd/bin
# Install
echo -n PATH='$PATH:' >> ~/.bashrc
echo $pwd/bin' >> ~/.bashrc
source ~/.bashrc# Optional function-syntax
$ cat echo.evm
CALLDATACOPY(RETURNDATASIZE, RETURNDATASIZE, CALLDATASIZE)
RETURN(RETURNDATASIZE, CALLDATASIZE)
# Outputs evm bytecode
$ evm echo.evm
363d3d37363df3
# Wrap the minimum viable constructor with -c
$ evm -c echo.evm
66363d3d37363df33d5260076019f3
# Labels are lowercase, opcodes are uppercase
$ cat infinite.evm
start:
JUMP(start)
$ evm infinite.evm
5b600056
# Use decimal and hexadecimal constants directly without explicitly specifying PUSHx width
$ cat add.evm
MSTORE(RETURNDATASIZE,ADD(CALLDATALOAD(RETURNDATASIZE),CALLDATALOAD(32)))
RETURN(RETURNDATASIZE,0x20)
$ evm add.evm
6020353d35013d5260203df3
# Can read from stdin
$ cat add.evm | evm
6020353d35013d5260203df3More examples can be found in the tst/in directory.
Besides instructions, you can inline raw data into the program using a data section, delimited with {}.
Data section items are labeled and comma-separated.
The location of a data section item can be accessed in the assembly by its label.
The size of a data section item can be accessed in the code with #.
Thereby, you can CODECOPY such data into memory.
CODECOPY(0, child, #child)
MSTORE(0, CREATE(0, #child))
RETURN(0, 32)
{
child: construct child.evm
}
| Item Type | Example Item | Example Data |
|---|---|---|
| Hex | balanceof: 0x70a08231 |
70a08231 |
| String | hello: "Hello, world!" |
48656c6c6f2c20776f726c6421 |
| Assembly | selfdestruct: assemble tst/in/selfdestruct.evm |
33ff |
| Construct | constructor: construct tst/in/selfdestruct.evm |
6133ff3d526002601ef3 |
Any item can be sliced with a Python-style [start:end] byte suffix to inline only a subset.
start defaults to 0 and end to the item length; either bound may be negative to count from the end.
| Sliced Item | Example Data |
|---|---|
runtime: assemble tst/in/selfdestruct.evm[1:] |
ff |
head: 0xdeadbeef[:2] |
dead |
tail: "Hello, world!"[-6:] |
776f726c6421 |
$ cat selfdestruct.out
33ff
# Outputs valid assembly
$ evm -d selfdestruct.out
SELFDESTRUCT(CALLER)
# Can read from stdin
$ cat selfdestruct.out | evm -d
SELFDESTRUCT(CALLER)$ cat quine.evm
CODECOPY(
RETURNDATASIZE,
RETURNDATASIZE,
CODESIZE
)
RETURN(
RETURNDATASIZE,
CODESIZE
)
# Executes the code and outputs the returndata
$ evm -c quine.evm | evm -x
383d3d39383df3
$ evm -c quine.evm | evm -x | evm -d
CODECOPY(RETURNDATASIZE,RETURNDATASIZE,CODESIZE)
RETURN(RETURNDATASIZE,CODESIZE)By using -w config.json, you can define the precondition state before execution.
The recommended way to generate this is bin/dio, which snapshots live chain state; see On-chain state below.
[
{
"address": "0x80d9b122dc3a16fdc41f96cf010ffe7e38d227c3",
"nonce": "0x",
"code": "0x383d3d39383df3",
"storage": {
"0x00" : "0xf1ecf98489fa9ed60a664fc4998db699cfa39d40",
"0x01" : "0x01"
}
}
]Account configuration fields are optional and default to zero. If you exclude address, one will be generated for you.
Besides declaring code, contracts can be constructed from assembly source.
If code is also supplied for the entry, the code will be used to verify the result of the constructor.
| Dio Key | Description | Example Values | Default Value or Behavior |
|---|---|---|---|
address |
address for the account | 0x83F20F44975D03b1b09e64809B757c47f942BEeA |
|
balance |
value in the account | 0xde0b6b3a7640000 |
0x0 |
nonce |
nonce of the account | 0x1 |
0x0 |
storage |
account storage | {"0x1":"0x115eec47f6cf7e35000000"} |
{} |
creator |
address of the account creating this contract | 0x3249936bDdF8bF739d3f06d26C40EEfC81029BD1 |
0x0000000000000000000000000000000000000000 |
initcode |
account creation code | 0x383d3d39383df3, tst/in/quine.evm |
code mocked without constructor |
construct |
specify initcode as minimum constructor of file |
tst/in/quine.evm |
initcode |
constructTest |
test constructor execution; shares fields of tests entries |
{"gasUsed": "0xd583"} |
none |
code |
account code ; validated if initcode specified |
0x383d3d39383df3, tst/in/quine.evm |
0x |
import |
load another configuration | tst/quine.json |
|
tests |
transactions executed sequentially, after account configuration | [ |
[] |
See the next section for test configuration.
dio drives evm -nx against a real node and emits a -w config JSON capturing every account, balance, nonce, code, and storage slot the call touched.
That config then replays offline and deterministically with evm -w.
dio links libcurl and execs the evm binary at runtime, so build both (make bin/evm bin/dio).
# provider URL positionally, or via $ETH_RPC_URL
echo '{"to":"0x6b175474e89094c44da98b954eedeac495271d0f","data":"0x18160ddd"}' \
| dio https://mainnet.infura.io/v3/KEY | jq
# write to a file instead of stdout
export ETH_RPC_URL=https://mainnet.infura.io/v3/KEY
dio $ETH_RPC_URL dai.json < call.json
# CREATE: omit "to"; with "from" the deployed address is derived from from+nonce
echo '{"from":"0xd8da6bf26964af9d7eed9e03e53415d37aa96045","data":"0x<initcode>"}' | dio $ETH_RPC_URLThe call JSON comes from -o, file arguments, or stdin, and may be a single object or an array of them.
Each object becomes a tests entry (or constructTest, for a CREATE) on the generated account.
Replay with -w runs the calls in their original order, after every account they fetched: each call is recorded on the account it calls, placed last, unless that would reorder calls, in which case it goes on the last entry with an explicit to.
| Call JSON key | Meaning | Default |
|---|---|---|
to |
contract called; omit for a CREATE | (CREATE) |
from |
msg.sender / deployer |
0x00…00 |
data / input |
calldata, or initcode when to is omitted |
0x |
value |
wei sent with the call | 0x0 |
block |
latest, or a 0x-prefixed hex block number, to pin state to |
latest |
nonce, chainId, blockOverrides, stateOverrides |
as in network mode |
Each entry records only the block values its call read, such as timestamp or chainId.
An entry's stateOverrides are recorded on its test, while each account records its chain state.
| dio argument | Meaning |
|---|---|
| 1st positional | provider URL (http(s):// or ws(s)://); falls back to $ETH_RPC_URL |
| 2nd positional | output file (default: stdout) |
| more positionals | files of call JSON to read |
-o <json> |
call JSON inline instead of stdin/file |
-h, --help |
usage |
[
{
"construct": "tst/in/quine.evm",
"code": "0x383d3d39383df3",
"tests": [
{
"name": "ignores calldata",
"input": "0xdeadbeef",
"output": "0x383d3d39383df3"
}
]
}
]Run unit tests with tests entries.
All -w output goes to stderr, while -x output goes to stdout.
evm -w tst/quine.json# tst/in/quine.evm
ignores calldata: pass
| Test Key | Description | Example Value | Default Value or Behavior |
|---|---|---|---|
name |
label for the test case | "decimals() = 18" |
index of the testcase |
input |
msg.data |
0x313ce567 |
0x |
value |
msg.value |
0x38d7ea4c68000 |
0x0 |
from |
tx.origin |
0xd1236a6A111879d9862f8374BA15344b6B233Fbd |
0x0000000000000000000000000000000000000000 |
nonce |
nonce of from before the call |
0x5 |
unchanged |
stateOverrides |
account state set before the call, as in network mode | {"0x4838B106FCe9647Bdf1E7877BF73cE8B0BAD5f97":{"balance":"0x1"}} |
{} |
gas |
tx.gasLimit |
0x5208 |
uint64(-1) |
op |
type of call | STATICCALL |
CALL |
to |
account called | 0x83F20F44975D03b1b09e64809B757c47f942BEeA |
account address |
status |
expected return status | 0x0 (revert) |
0x1 (success) |
output |
expected return or revert data | 0x0000000000000000000000000000000000000000000000000000000000000012 |
ignored |
logs |
expected logs by account | { |
ignored |
gasUsed |
expected gas used | 0x5208 |
ignored |
accessList |
EIP-2929 | [{"0x22d8432cc7aa4f8712a655fc4cdfb1baec29fca9":["0x6"]}] |
{} |
blockNumber |
block.number |
0x1312d00 |
0x13a2228 |
timestamp |
block.timestamp |
0x68255820 |
0x65712600 |
gasLimit |
block.gaslimit |
0x2faf080 |
0x1c9c380 |
chainId |
block.chainid |
0x2105 |
0x1 |
baseFee |
block.basefee |
0x3b9aca00 |
0x7 |
blobBaseFee |
block.blobbasefee |
0x2 |
0x1 |
prevRandao |
block.prevrandao |
0x1234 |
0x0 |
coinbase |
block.coinbase |
0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5 |
0x4838B106FCe9647Bdf1E7877BF73cE8B0BAD5f97 |
debug |
debug flags | 0x20 |
0x0 |
Block values and stateOverrides set by a test persist to later tests.
The current debug flags:
| Debug Flag | Description |
|---|---|
| 0x1 | Stack |
| 0x2 | Memory |
| 0x4 | Opcodes |
| 0x8 | Gas |
| 0x10 | PC |
| 0x20 | Calls |
| 0x40 | Logs |
These flags can also be set with -D, which are or'd with the debug flags of each test.
-D -1 enables every flag.
A gasUsed test field can be supplied (or updated) in-place with -u
evm -uw tst/quine.json
git diff tst/quine.jsondiff --git a/tst/quine.json b/tst/quine.json
index 361c65f..1e8a9f4 100644
--- a/tst/quine.json
+++ b/tst/quine.json
@@ -5,6 +5,7 @@
"tests": [
{
"name": "ignores calldata",
+ "gasUsed": "0x525b",
"input": "0xdeadbeef",
"output": "0x383d3d39383df3"
}Using any of the following -x options will output JSON instead of the returndata.
The JSON will always contain the returndata but other outputs can be specified.
-g: gasUsed-l: logs-s: status
-t emits an EIP-3155 JSON trace for -x and -w: one line per step, then a summary line per transaction.
evm -txo 385952593df3 2>&1 >/dev/null{"pc":0,"op":56,"gas":"0xffffffffffff3095","stack":[],"depth":1,"returnData":"0x","refund":0,"memSize":0,"opName":"CODESIZE","gasCost":"0x2"}
{"pc":1,"op":89,"gas":"0xffffffffffff3093","stack":["0x6"],"depth":1,"returnData":"0x","refund":0,"memSize":0,"opName":"MSIZE","gasCost":"0x2"}
{"pc":2,"op":82,"gas":"0xffffffffffff3091","stack":["0x6","0x0"],"depth":1,"returnData":"0x","refund":0,"memSize":0,"opName":"MSTORE","gasCost":"0x6"}
{"pc":3,"op":89,"gas":"0xffffffffffff308b","stack":[],"depth":1,"returnData":"0x","refund":0,"memSize":32,"opName":"MSIZE","gasCost":"0x2"}
{"pc":4,"op":61,"gas":"0xffffffffffff3089","stack":["0x20"],"depth":1,"returnData":"0x","refund":0,"memSize":32,"opName":"RETURNDATASIZE","gasCost":"0x2"}
{"pc":5,"op":243,"gas":"0xffffffffffff3087","stack":["0x20","0x0"],"depth":1,"returnData":"0x","refund":0,"memSize":32,"opName":"RETURN","gasCost":"0x0"}
{"output":"0x0000000000000000000000000000000000000000000000000000000000000006","gasUsed":"0xe878","pass":true}The gasCost of a CALL or CREATE step includes the gas it forwards.
The summary line omits stateRoot.
-m adds the optional memory field to each step.
-t overrides any debug flags from -w tests, and cannot be combined with -D.
Trace and debug both append to the file specified by -T, else stderr.
evm -nx executes against live chain state.
Rather than declaring every touched account and storage slot up front with -w, the interpreter fetches them on demand.
Each fetch is a JSON-RPC request written to stdout; the matching response is read back from stdin.
evm never opens a socket itself.
Use dio to forward requests to an Ethereum RPC.
The bytecode input becomes a JSON call object (the eth_call shape), one per line, each sharing the same EVM state:
{"to":"0x6b175474e89094c44da98b954eedeac495271d0f","from":"0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266","data":"0x18160ddd"}Omit to to deploy data as initcode.
Call objects also work with -x alone.
| Call Key | Description |
|---|---|
to |
account called; omit to deploy data |
from |
tx.origin |
data or input |
calldata, or initcode |
value |
msg.value |
gas |
gas limit for this call; defaults to uint64(-1) |
nonce |
the nonce of from before this call; it persists |
chainId |
block.chainid for this call only |
blockOverrides |
block values for this call only, keyed like geth's eth_call: number, time, gasLimit, baseFeePerGas, blobBaseFee, prevRandao, feeRecipient |
stateOverrides |
account state set before this call, keyed like geth's eth_call: balance, nonce, code, and either state or stateDiff |
Request emitted by evm |
When |
|---|---|
eth_blockNumber |
once, on the first fetch or NUMBER |
eth_getCode + eth_getTransactionCount + eth_getBalance |
first touch of an account (sent as one batch array, without overridden fields) |
eth_getStorageAt |
first read of a storage slot |
eth_chainId |
once, on the first CHAINID |
eth_getBlockByNumber |
once per block, on the first TIMESTAMP, GASLIMIT, BASEFEE, PREVRANDAO, or COINBASE |
Accounts created during execution are served locally and never fetched.
Unlike blockOverrides, stateOverrides persist to later calls.
Overridden fields and slots are never fetched; an account with balance, nonce, and code all overridden is not fetched at all.
stateDiff sets the listed slots, while state replaces the account's storage, so that unlisted slots read as zero and are never fetched.
A nonce override for from must match the call's nonce.
Overriding number to N fetches the header of block N and state at block N - 1.
If block N does not exist yet, its header fields fall back to their defaults with a warning.
Accounts and storage are fetched once per process, so later calls reuse them regardless of number.
blobBaseFee is not fetched; override it.
Until the coinbase is known, accessing it costs the cold surcharge.
With -n, JSON output reports the block values each call read, in the same chainId and blockOverrides keys, so they can be replayed.
EVM execution should mostly work but may not implement every opcode and corner-case. If you find a bug that disrupts you, please file an issue with its impact to you and code that reproduces it and I may find time to fix it, or alternatively you can submit a pull request.
- Implicit stack use
- Stack underflow warnings
- Explicit Constructor
- Label JUMPDEST in JUMPI and JUMP args
- Flags to include state changes in JSON
- Mock calls via
-wconfig
| Opname | Assembly and Disassembly | Execution |
|---|---|---|
| STOP | ✅ | ✅ |
| ADD | ✅ | ✅ |
| MUL | ✅ | ✅ |
| SUB | ✅ | ✅ |
| DIV | ✅ | ✅ |
| SDIV | ✅ | ✅ |
| MOD | ✅ | ✅ |
| SMOD | ✅ | ✅ |
| ADDMOD | ✅ | ✅ |
| MULMOD | ✅ | ✅ |
| EXP | ✅ | ✅ |
| SIGNEXTEND | ✅ | ✅ |
| LT | ✅ | ✅ |
| GT | ✅ | ✅ |
| SLT | ✅ | ✅ |
| SGT | ✅ | ✅ |
| EQ | ✅ | ❓ |
| ISZERO | ✅ | ✅ |
| AND | ✅ | ❓ |
| OR | ✅ | ❓ |
| XOR | ✅ | ✅ |
| NOT | ✅ | ❓ |
| BYTE | ✅ | ✅ |
| SHL | ✅ | ✅ |
| SHR | ✅ | ✅ |
| SAR | ✅ | ❓ |
| CLZ | ✅ | ✅ |
| SHA3 | ✅ | ✅ |
| ADDRESS | ✅ | ✅ |
| BALANCE | ✅ | ✅ |
| ORIGIN | ✅ | ❓ |
| CALLER | ✅ | ✅ |
| CALLVALUE | ✅ | ✅ |
| CALLDATALOAD | ✅ | ✅ |
| CALLDATASIZE | ✅ | ✅ |
| CALLDATACOPY | ✅ | ✅ |
| CODESIZE | ✅ | ✅ |
| CODECOPY | ✅ | ✅ |
| GASPRICE | ✅ | ❌ |
| EXTCODESIZE | ✅ | ✅ |
| EXTCODECOPY | ✅ | ✅ |
| RETURNDATASIZE | ✅ | ✅ |
| RETURNDATACOPY | ✅ | ✅ |
| EXTCODEHASH | ✅ | ✅ |
| BLOCKHASH | ✅ | ❌ |
| COINBASE | ✅ | ✅ |
| TIMESTAMP | ✅ | ✅ |
| NUMBER | ✅ | ✅ |
| PREVRANDAO | ✅ | ✅ |
| GASLIMIT | ✅ | ✅ |
| CHAINID | ✅ | ✅ |
| SELFBALANCE | ✅ | ✅ |
| BASEFEE | ✅ | ✅ |
| BLOBHASH | ✅ | ❌ |
| BLOBBASEFEE | ✅ | ✅ |
| POP | ✅ | ✅ |
| MLOAD | ✅ | ✅ |
| MSTORE | ✅ | ✅ |
| MSTORE8 | ✅ | ✅ |
| SLOAD | ✅ | ✅ |
| SSTORE | ✅ | ✅ |
| JUMP | ✅ | ✅ |
| JUMPI | ✅ | ✅ |
| PC | ✅ | ✅ |
| MSIZE | ✅ | ✅ |
| GAS | ✅ | ✅ |
| JUMPDEST | ✅ | ✅ |
| TLOAD | ✅ | ✅ |
| TSTORE | ✅ | ✅ |
| MCOPY | ✅ | ❓ |
| PUSH0 | ✅ | ✅ |
| PUSH1 | ✅ | ✅ |
| PUSH2 | ✅ | ✅ |
| PUSH3 | ✅ | ✅ |
| PUSH4 | ✅ | ✅ |
| PUSH5 | ✅ | ❓ |
| PUSH6 | ✅ | ❓ |
| PUSH7 | ✅ | ✅ |
| PUSH8 | ✅ | ❓ |
| PUSH9 | ✅ | ❓ |
| PUSH10 | ✅ | ❓ |
| PUSH11 | ✅ | ❓ |
| PUSH12 | ✅ | ❓ |
| PUSH13 | ✅ | ❓ |
| PUSH14 | ✅ | ❓ |
| PUSH15 | ✅ | ✅ |
| PUSH16 | ✅ | ❓ |
| PUSH17 | ✅ | ❓ |
| PUSH18 | ✅ | ✅ |
| PUSH19 | ✅ | ❓ |
| PUSH20 | ✅ | ✅ |
| PUSH21 | ✅ | ❓ |
| PUSH22 | ✅ | ❓ |
| PUSH23 | ✅ | ✅ |
| PUSH24 | ✅ | ❓ |
| PUSH25 | ✅ | ❓ |
| PUSH26 | ✅ | ❓ |
| PUSH27 | ✅ | ❓ |
| PUSH28 | ✅ | ❓ |
| PUSH29 | ✅ | ❓ |
| PUSH30 | ✅ | ❓ |
| PUSH31 | ✅ | ❓ |
| PUSH32 | ✅ | ✅ |
| DUP1 | ✅ | ✅ |
| DUP2 | ✅ | ✅ |
| DUP3 | ✅ | ✅ |
| DUP4 | ✅ | ✅ |
| DUP5 | ✅ | ✅ |
| DUP6 | ✅ | ✅ |
| DUP7 | ✅ | ❓ |
| DUP8 | ✅ | ❓ |
| DUP9 | ✅ | ❓ |
| DUP10 | ✅ | ❓ |
| DUP11 | ✅ | ❓ |
| DUP12 | ✅ | ❓ |
| DUP13 | ✅ | ❓ |
| DUP14 | ✅ | ❓ |
| DUP15 | ✅ | ❓ |
| DUP16 | ✅ | ❓ |
| SWAP1 | ✅ | ✅ |
| SWAP2 | ✅ | ✅ |
| SWAP3 | ✅ | ❓ |
| SWAP4 | ✅ | ❓ |
| SWAP5 | ✅ | ❓ |
| SWAP6 | ✅ | ❓ |
| SWAP7 | ✅ | ❓ |
| SWAP8 | ✅ | ❓ |
| SWAP9 | ✅ | ❓ |
| SWAP10 | ✅ | ❓ |
| SWAP11 | ✅ | ❓ |
| SWAP12 | ✅ | ❓ |
| SWAP13 | ✅ | ❓ |
| SWAP14 | ✅ | ❓ |
| SWAP15 | ✅ | ❓ |
| SWAP16 | ✅ | ❓ |
| LOG0 | ✅ | ✅ |
| LOG1 | ✅ | ✅ |
| LOG2 | ✅ | ✅ |
| LOG3 | ✅ | ✅ |
| LOG4 | ✅ | ✅ |
| CREATE | ✅ | ✅ |
| CALL | ✅ | ✅ |
| CALLCODE | ✅ | ❌ |
| RETURN | ✅ | ✅ |
| DELEGATECALL | ✅ | ✅ |
| CREATE2 | ✅ | ✅ |
| STATICCALL | ✅ | ✅ |
| REVERT | ✅ | ✅ |
| INVALID | ✅ | ❌ |
| SELFDESTRUCT | ✅ | ❌ |
| Precompile | Address | Execution Supported |
|---|---|---|
HOLE |
0x0 |
✅ |
ECRECOVER |
0x1 |
✅ |
SHA2_256 |
0x2 |
❌ |
RIPEMD160 |
0x3 |
❌ |
IDENTITY |
0x4 |
✅ |
MODEXP |
0x5 |
❌ |
EC_ADD |
0x6 |
❌ |
EC_MUL |
0x7 |
❌ |
EC_PAIRING |
0x8 |
❌ |
BLACK2F |
0x9 |
❌ |
ZKG_POINT |
0xa |
❌ |
Please use camelCase for methods and variables but snake_case for types. Write errors to stderr. Use the C preprocessor.
-
make: build the things -
make again: rebuild the things -
make clean: remove the things -
make check: run the tests -
make distcheck: run all the tests
If you are fixing a bug or adding a feature, first write a test in the tst directory that fails now but would pass after your change.
Lastly, verify your test now passes using make check.
Some unit tests written in C are found in tst/*.c.
If they pass, they should have no output and exit with a status of 0.
Assembler tests live in tst/in/*.evm.
The assembler assembles those files and compares the output to the corresponding file in tst/out/.
EVM execution tests use the dio testing system.
They can be found at tst/*.json.
They can be run individually with evm -w.
The README.md is assembled by concatentation when make.
See the Makefile.
If you want to update the opcodes supported table, it should be updated automatically.
Otherwise, the files you want to edit are:
make/.rme.mdmake/ops.shCONTRIBUTING.md
