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

Improve docs #690

Merged
merged 6 commits into from
Mar 24, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
180 changes: 138 additions & 42 deletions doc/src/equivalence.md
Original file line number Diff line number Diff line change
@@ -1,53 +1,149 @@
# `hevm equivalence`

```
Usage: hevm equivalence --code-a TEXT --code-b TEXT [--sig TEXT]
[--arg STRING]... [--calldata TEXT]
[--smttimeout NATURAL] [--max-iterations INTEGER]
[--solver TEXT] [--smtoutput] [--smtdebug] [--debug]
[--trace] [--ask-smt-iterations INTEGER]
[--num-cex-fuzz INTEGER]
[--loop-detection-heuristic LOOPHEURISTIC]
[--abstract-arithmetic] [--abstract-memory]

Available options:
-h,--help Show this help text
--code-a TEXT Bytecode of the first program
--code-b TEXT Bytecode of the second program
--sig TEXT Signature of types to decode / encode
--arg STRING Values to encode
--calldata TEXT Tx: calldata
--smttimeout NATURAL Timeout given to SMT solver in seconds (default: 300)
--max-iterations INTEGER Number of times we may revisit a particular branching
point. Default is 5. Setting to -1 allows infinite looping
--solver TEXT Used SMT solver: z3 (default), cvc5, or bitwuzla
--smtoutput Print verbose smt output
--smtdebug Print smt queries sent to the solver
--debug Debug printing of internal behaviour
--trace Dump trace
--ask-smt-iterations INTEGER
Number of times we may revisit a particular branching
point before we consult the smt solver to check
reachability (default: 1) (default: 1)
--num-cex-fuzz INTEGER Number of fuzzing tries to do to generate a
counterexample (default: 3) (default: 3)
--loop-detection-heuristic LOOPHEURISTIC
Which heuristic should be used to determine if we are
in a loop: StackBased (default) or Naive
(default: StackBased)
```plain
Usage: hevm equivalence [--code-a TEXT] [--code-b TEXT] [--code-a-file STRING]
[--code-b-file STRING] [--sig TEXT] [--arg STRING]...
[--calldata TEXT] [--smttimeout NATURAL]
[--max-iterations INTEGER] [--solver TEXT]
[--num-solvers NATURAL] ...
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does ... mean here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to imply that there are more options... maybe I should remove? I don't want to list them all, I think it's not that useful? What do you think?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not document all the options?
It seems unusual to omit some of them.
Unless you can argue they belong to separate categories, like "for users" and "for developers"?
And you want to list only those important for the users.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, we keep adding some and then not documenting them in the MD docs. I am wondering what' the best thing to do here. Of course I can run --help and copy-paste but I'm not sure that's the best. I am trying to improve the documentation, not make it perfect :D Currently, it's quite out of date. I am not sure what's best. The full --help is also way too verbose and I don't think it's too helpful. They can always run it themselves. What do you think? Should I just copy-paste the --help and then explain the options as it is now (with common-options.md to explain the common ones)?

```

Symbolically execute both the code given in `--code-a` and `--code-b` and try
to prove equivalence between their outputs and storages. Extracting bytecode
from solidity contracts can be done via, e.g.:
to prove equivalence between their outputs and storages. For a full listing of
options, see `hevm equivalence --help`.

## Simple example usage

```shell
$ solc --bin-runtime "contract1.sol" | tail -n1 > a.bin
$ solc --bin-runtime "contract2.sol" | tail -n1 > b.bin
$ hevm equivalence --code-a-file a.txt --code-b-file b.txt
```
hevm equivalence \
--code-a $(solc --bin-runtime "contract1.sol" | tail -n1) \
--code-b $(solc --bin-runtime "contract2.sol" | tail -n1)
```

## Calldata size limits

If `--sig` is given, calldata is assumed to take the form of the function
given. If `--calldata` is provided, a specific, concrete calldata is used. If
neither is provided, a fully abstract calldata of at most `2**64` byte is
assumed. Note that a `2**64` byte calldata would go over the gas limit, and
hence should cover all meaningful cases.
hence should cover all meaningful cases. You can limit the buffer size via
`--max-buf-size`, which sets the exponent of the size, i.e. 10 would limit the
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we call this option --max-calldata-buf-size?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! Yes, we should. Fixing.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah wait... this actually applies to other buffers too, like here:

symCalldata :: App m => Text -> [AbiType] -> [String] -> Expr Buf -> m (Expr Buf, [Prop])
symCalldata sig typesignature concreteArgs base = do
  conf <- readConfig
  let
    args = concreteArgs <> replicate (length typesignature - length concreteArgs) "<symbolic>"
    mkArg :: AbiType -> String -> Int -> CalldataFragment
    mkArg typ "<symbolic>" n = symAbiArg (T.pack $ "arg" <> show n) typ
    mkArg typ arg _ =
      case makeAbiValue typ arg of
        AbiUInt _ w -> St [] . Lit . into $ w
        AbiInt _ w -> St [] . Lit . unsafeInto $ w
        AbiAddress w -> St [] . Lit . into $ w
        AbiBool w -> St [] . Lit $ if w then 1 else 0
        _ -> internalError "TODO"
    calldatas = zipWith3 mkArg typesignature args [1..]
    (cdBuf, props) = combineFragments calldatas base
    withSelector = writeSelector cdBuf sig
    sizeConstraints
      = (Expr.bufLength withSelector .>= cdLen calldatas)
      .&& (Expr.bufLength withSelector .< (Lit (2 ^ conf.maxBufSize)))

and also the data that an unknown contract sends back to us. We should explain this...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I am gonna talk about this more in the docs and try to give a better explanation in the --help, too.

calldata to `2**10` bytes.

## What constitutes equivalence

The equivalence checker considers two contracts equivalent if given the
same calldata they:
- return the same value
- have the same storage
- match on the success/failure of the execution
Importantly, logs are *not* considered in the equivalence check. Hence,
it is possible that two contracts are considered equivalent by `hevm equivalence` but
they emit different log items.

For example, two contracts that are:

```
PUSH1 3
```

And

```
PUSH1 4
```

Are considered *equivalent*, because they don't put anything in the return
data, are not different in their success/fail attribute, and don't touch
storage. However, these two are considered different:

```
PUSH1 3
PUSH1 0x20
MSTORE
PUSH1 0x40
PUSH1 0x00
RETURN
```

and:


```
PUSH1 4
PUSH1 0x20
MSTORE
PUSH1 0x40
PUSH1 0x00
RETURN
```

Since one of them returns a 3 and the other a 4. We also consider contracts different when
they differ in success/fail. So these two contracts:

```
PUSH1 0x00
PUSH1 0x00
RETURN
```

and:

```
PUSH1 0x00
PUSH1 0x00
REVERT
```

Are considered different, as one of them reverts (i.e. fails) and the other
succeeds.

## Creation code equivalence

If you want to check the equivalence of not just the runtime code, but also the
creation code of two contracts, you can use the `--creation` flag. For example
these two contracts:

```solidity
contract C {
uint private immutable NUMBER;
constructor(uint a) {
NUMBER = 2;
}
function stuff(uint b) public returns (uint256) {
unchecked{return 2+NUMBER+b;}
}
}
```

And:

```solidity
contract C {
uint private immutable NUMBER;
constructor(uint a) {
NUMBER = 4;
}
function stuff(uint b) public returns (uint256) {
unchecked {return NUMBER+b;}
}
}
```

Will compare equal when compared with `--create` flag:

```shell
solc --bin a.sol | tail -n1 > a.bin
solc --bin b.sol | tail -n1 > b.bin
cabal run exe:hevm equivalence -- --code-a-file a.bin --code-b-file b.bin --create
```

Notice that we used `--bin` and not `--bin-runtime` for solc here. Also note that
in case `NUMBER` is declared `public`, the two contracts will not be considered
equivalent, since solidity will generate a getter for `NUMBER`, which will
return 2/4 respectively.

## Further reading

For a tutorial on how to use `hevm equivalence`, see the [equivalence checking
tutorial](symbolic-execution-tutorial.html).
51 changes: 11 additions & 40 deletions doc/src/exec.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,57 +2,28 @@

Run an EVM computation using specified parameters.

```
Usage: hevm exec [--code TEXT] [--calldata TEXT] [--address ADDR]
```plain
Usage: hevm exec [--code TEXT] [--code-file STRING] [--calldata TEXT] [--address ADDR]
[--caller ADDR] [--origin ADDR] [--coinbase ADDR]
[--value W256] [--nonce WORD64] [--gas WORD64] [--number W256]
[--timestamp W256] [--basefee W256] [--priority-fee W256]
[--gaslimit WORD64] [--gasprice W256] [--create]
[--gaslimit WORD64] [--gasprice W256]
[--maxcodesize W256] [--prev-randao W256] [--chainid W256]
[--debug] [--trace] [--rpc TEXT] [--block W256] [--root STRING]
[--project-type PROJECTTYPE]

Available options:
-h,--help Show this help text
--code TEXT Program bytecode
--calldata TEXT Tx: calldata
--address ADDR Tx: address
--caller ADDR Tx: caller
--origin ADDR Tx: origin
--coinbase ADDR Block: coinbase
--value W256 Tx: Eth amount
--nonce WORD64 Nonce of origin
--gas WORD64 Tx: gas amount
--number W256 Block: number
--timestamp W256 Block: timestamp
--basefee W256 Block: base fee
--priority-fee W256 Tx: priority fee
--gaslimit WORD64 Tx: gas limit
--gasprice W256 Tx: gas price
--create Tx: creation
--maxcodesize W256 Block: max code size
--prev-randao W256 Block: prevRandao
--chainid W256 Env: chainId
--debug Debug printing of internal behaviour
--trace Dump trace
--rpc TEXT Fetch state from a remote node
--block W256 Block state is be fetched from
--root STRING Path to project root directory (default: . )
--project-type PROJ Foundry or CombinedJSON project (default: Foundry)
--assertion-type ASSERT Assertions as per Forge or DSTest (default: Forge)
[--trace] [--rpc TEXT] [--block W256] ...
```

Minimum required flags: either you must provide `--code` or you must both pass
`--rpc` and `--address`.
Concretely execute a given EVM bytecode with the specified parameters. Minimum
required flags: either you must provide `--code` or you must both pass `--rpc`
and `--address`. For a full listing of options, see `hevm exec --help`.

If the execution returns an output, it will be written
to stdout. Exit code indicates whether the execution was successful or
errored/reverted.

Simple example usage:
## Simple example usage

```
hevm exec --code 0x647175696e6550383480393834f3 --gas 0xff
```shell
$ hevm exec --code 0x647175696e6550383480393834f3 --gas 0xff
"Return: 0x647175696e6550383480393834f3"
```

Expand All @@ -61,7 +32,7 @@ Virtual Machine will put `0x647175696e6550383480393834f3` in the RETURNDATA.

To execute a mainnet transaction:

```
```shell
# install seth as per
# https://github.com/makerdao/developerguides/blob/master/devtools/seth/seth-guide/seth-guide.md
$ export ETH_RPC_URL=https://mainnet.infura.io/v3/YOUR_API_KEY_HERE
Expand Down
91 changes: 23 additions & 68 deletions doc/src/symbolic.md
Original file line number Diff line number Diff line change
@@ -1,83 +1,24 @@
# `hevm symbolic`

```shell
Usage: hevm symbolic [--code TEXT] [--calldata TEXT] [--address ADDR]
```plain
Usage: hevm symbolic [--code TEXT] [--code-file STRING] [--calldata TEXT] [--address ADDR]
[--caller ADDR] [--origin ADDR] [--coinbase ADDR]
[--value W256] [--nonce WORD64] [--gas WORD64]
[--number W256] [--timestamp W256] [--basefee W256]
[--priority-fee W256] [--gaslimit WORD64] [--gasprice W256]
[--create] [--maxcodesize W256] [--prev-randao W256]
[--chainid W256] [--rpc TEXT] [--block W256]
[--root STRING] [--project-type PROJECTTYPE]
[--initial-storage INITIALSTORAGE] [--sig TEXT]
[--arg STRING]... [--get-models] [--show-tree]
[--show-reachable-tree] [--smttimeout NATURAL]
[--max-iterations INTEGER] [--solver TEXT] [--smtdebug]
[--assertions [WORD256]] [--ask-smt-iterations INTEGER]
[--num-solvers NATURAL]
[--loop-detection-heuristic LOOPHEURISTIC]

Available options:
-h,--help Show this help text
--code TEXT Program bytecode
--calldata TEXT Tx: calldata
--address ADDR Tx: address
--caller ADDR Tx: caller
--origin ADDR Tx: origin
--coinbase ADDR Block: coinbase
--value W256 Tx: Eth amount
--nonce WORD64 Nonce of origin
--gas WORD64 Tx: gas amount
--number W256 Block: number
--timestamp W256 Block: timestamp
--basefee W256 Block: base fee
--priority-fee W256 Tx: priority fee
--gaslimit WORD64 Tx: gas limit
--gasprice W256 Tx: gas price
--create Tx: creation
--maxcodesize W256 Block: max code size
--prev-randao W256 Block: prevRandao
--chainid W256 Env: chainId
--rpc TEXT Fetch state from a remote node
--block W256 Block state is be fetched from
--root STRING Path to project root directory (default: . )
--project-type PROJECTTYPE Foundry or CombinedJSON project
--initial-storage INITIALSTORAGE
Starting state for storage: Empty, Abstract (default
Abstract)
--sig TEXT Signature of types to decode / encode
--arg STRING Values to encode
--get-models Print example testcase for each execution path
--show-tree Print branches explored in tree view
--show-reachable-tree Print only reachable branches explored in tree view
--smttimeout NATURAL Timeout given to SMT solver in seconds (default: 300)
--max-iterations INTEGER Number of times we may revisit a particular branching
point. Default is 5. Setting to -1 allows infinite looping
--solver TEXT Used SMT solver: z3 (default), cvc5, or bitwuzla
--smtdebug Print smt queries sent to the solver
--assertions [WORD256] Comma separated list of solc panic codes to check for
(default: user defined assertion violations only)
--ask-smt-iterations INTEGER
Number of times we may revisit a particular branching
point before we consult the smt solver to check
reachability (default: 1) (default: 1)
--num-solvers NATURAL Number of solver instances to use (default: number of
cpu cores)
--loop-detection-heuristic LOOPHEURISTIC
Which heuristic should be used to determine if we are
in a loop: StackBased (default) or Naive
(default: StackBased)
[--number W256] [--timestamp W256] [--basefee W256] ..
```

Run a symbolic execution against the given parameters, searching for assertion violations.
Run a symbolic execution against the given parameters, searching for assertion
violations. For a full listing of options, see `hevm symbolic --help`.

Counterexamples will be returned for any reachable assertion violations. Where
Counterexamples are returned for any reachable assertion violations. Where
an assertion violation is defined as either an execution of the invalid opcode
(`0xfe`), or a revert with a message of the form
`abi.encodeWithSelector('Panic(uint256)', errCode)` with `errCode` being one of
the predefined solc assertion codes defined
[here](https://docs.soliditylang.org/en/latest/control-structures.html#panic-via-assert-and-error-via-require).

## Arithmetic overflow

By default hevm ignores assertion violations that result from arithmetic
overflow (`Panic(0x11)`), although this behaviour can be customised via the
`--assertions` flag. For example, the following will return counterexamples for
Expand All @@ -87,7 +28,10 @@ arithmetic overflow (`0x11`) and user defined assertions (`0x01`):
hevm symbolic --code $CODE --assertions '[0x01, 0x11]'
```

The default value for `calldata` and `caller` are symbolic values, but can be specialized to concrete functions with their corresponding flags.
The default value for `calldata` and `caller` are symbolic values, but can be
specialized to concrete functions with their corresponding flags.

## Specializing calldata

One can also specialize specific arguments to a function signature, while
leaving others abstract. If `--sig` is given, calldata is assumed to be of the
Expand Down Expand Up @@ -126,11 +70,15 @@ The default timeout for SMT queries is no timeout. If your program is taking
longer than a couple of minutes to run, you can experiment with configuring the
timeout to somewhere around 10s by doing `--smttimeout 10000`

## Storage

Storage can be initialized in two ways:

- `Empty`: all storage slots for all contracts are initialized to zero
- `Abstract`: all storage slots are initialized as unconstrained abstract values

## Exploration strategy

`hevm` uses an eager approach for symbolic execution, meaning that it will
first attempt to explore all branches in the program (without querying the smt
solver to check if they are reachable or not). Once the full execution tree has
Expand All @@ -143,3 +91,10 @@ iterations (controlled by the `--ask-smt-iterations` flag), the solver will be
invoked to check whether a given loop branch is reachable. In cases where the
number of loop iterations is known in advance, you may be able to speed up
execution by setting this flag to an appropriate value.

## Further reading

For a tutorial on symbolic execution with `hevm`, see the [the page
here](symbolic-execution-tutorial.html).
An older blog post on symbolic execution with `hevm` can be found
[here](https://fv.ethereum.org/2020/07/28/symbolic-hevm-release).
Loading
Loading