├── .envrc ├── .github ├── pull_request_template.md ├── scripts │ ├── install-libff.sh │ ├── install-libsecp256k1.sh │ ├── libff.patch │ └── windows-ghc-toolchain.cmake └── workflows │ ├── bitwuzla-windows.yml │ ├── build.yml │ ├── check-dependencies.yml │ ├── publish-docs.yml │ └── release.yml ├── .gitignore ├── .hlint.yaml ├── CHANGELOG.md ├── COPYRIGHT ├── LICENSE ├── bench ├── bench-perf.hs ├── bench.hs └── contracts │ ├── deposit.sol │ ├── erc20.sol │ └── vat.sol ├── cli └── cli.hs ├── doc ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── book.css ├── book.toml ├── cspell.json ├── mermaid-init.js ├── mermaid.min.js ├── package-lock.json ├── package.json ├── readme.md ├── solidity.min.js ├── src │ ├── SUMMARY.md │ ├── architecture.md │ ├── common-options.md │ ├── equivalence-checking-tutorial.md │ ├── equivalence.md │ ├── exec.md │ ├── expr.png │ ├── for-hevm-developers.md │ ├── getting-started.md │ ├── install.md │ ├── limitations-and-workarounds.md │ ├── std-test-tutorial.md │ ├── symbolic-execution-tutorial.md │ ├── symbolic.md │ ├── test.md │ └── when-to-use.md ├── theme │ ├── highlight.js │ └── theme.js └── yarn.lock ├── ethjet ├── blake2.c ├── blake2.h ├── ethjet-ff.cc ├── ethjet-ff.h ├── ethjet.c ├── ethjet.h ├── tinykeccak.c └── tinykeccak.h ├── flake.lock ├── flake.nix ├── hevm.cabal ├── hie.yaml ├── readme.md ├── src ├── EVM.hs └── EVM │ ├── ABI.hs │ ├── Assembler.hs │ ├── CSE.hs │ ├── CheatsTH.hs │ ├── Concrete.hs │ ├── Dapp.hs │ ├── Effects.hs │ ├── Exec.hs │ ├── Expr.hs │ ├── Facts.hs │ ├── FeeSchedule.hs │ ├── Fetch.hs │ ├── Format.hs │ ├── Fuzz.hs │ ├── Keccak.hs │ ├── Op.hs │ ├── Precompiled.hs │ ├── RLP.hs │ ├── SMT.hs │ ├── Sign.hs │ ├── Solidity.hs │ ├── Solvers.hs │ ├── Stepper.hs │ ├── SymExec.hs │ ├── Transaction.hs │ ├── Traversals.hs │ ├── Types.hs │ └── UnitTest.hs └── test ├── BlockchainTests.hs ├── EVM └── Test │ ├── BlockchainTests.hs │ ├── Tracing.hs │ └── Utils.hs ├── clitest.hs ├── contracts ├── fail │ ├── 10_BadVault.sol │ ├── check-prefix.sol │ ├── dsProveFail.sol │ └── trivial.sol ├── lib │ ├── erc20.sol │ └── test.sol └── pass │ ├── abstract.sol │ ├── cheatCodes.sol │ ├── cheatCodesFork.sol │ ├── constantinople.sol │ ├── constantinople_min.sol │ ├── dsProvePass.sol │ ├── libraries.sol │ ├── loops.sol │ ├── no-overapprox-delegatecall.sol │ ├── no-overapprox-staticcall.sol │ ├── rpc.sol │ ├── transfer.sol │ ├── trivial.sol │ └── unwind.sol ├── rpc.hs ├── scripts └── convert_trace_to_json.sh └── test.hs /.envrc: -------------------------------------------------------------------------------- 1 | use_flake 2 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | ## Checklist 4 | 5 | - [ ] tested locally 6 | - [ ] added automated tests 7 | - [ ] updated the docs 8 | - [ ] updated the changelog 9 | -------------------------------------------------------------------------------- /.github/scripts/install-libff.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eux -o pipefail 3 | 4 | ## The following script builds and installs libff to ~/.local/lib 5 | 6 | INSTALL_VERSION=0.2.1 7 | 8 | if [[ "$(uname -s)" =~ ^MSYS_NT.* ]]; then 9 | echo "This script is only meant to run on Windows under MSYS2" 10 | exit 1 11 | fi 12 | 13 | if [ -d libff ]; then 14 | echo "$(pwd)/libff" already exists! Using it instead of re-cloning the repo. 15 | else 16 | git clone https://github.com/scipr-lab/libff -b "v$INSTALL_VERSION" --recursive 17 | fi 18 | cd libff 19 | git checkout "v$INSTALL_VERSION" && git submodule init && git submodule update 20 | 21 | patch -p1 < ../.github/scripts/libff.patch 22 | 23 | sed -i 's/find_library(GMP_LIBRARY gmp)/find_library(GMP_LIBRARY NAMES libgmp.a)/' CMakeLists.txt 24 | # This ends up causing the system headers to be included with -I and 25 | # thus they override the GHC mingw compiler ones. So this removes it 26 | # and re-adds the include with idirafter via the toolchain file 27 | sed -i '/INCLUDE_DIRECTORIES.*OPENSSL_INCLUDE_DIR/d' CMakeLists.txt 28 | PREFIX="$HOME/.local" 29 | ARGS=("-DCMAKE_INSTALL_PREFIX=$PREFIX" "-DWITH_PROCPS=OFF" "-G" "Ninja" "-DCMAKE_TOOLCHAIN_FILE=$PWD/../.github/scripts/windows-ghc-toolchain.cmake") 30 | CXXFLAGS="-fPIC" 31 | 32 | mkdir -p build 33 | cd build 34 | CXXFLAGS="$CXXFLAGS" cmake -DCMAKE_POLICY_VERSION_MINIMUM=3.5 "${ARGS[@]}" .. 35 | cmake --build . && cmake --install . 36 | -------------------------------------------------------------------------------- /.github/scripts/install-libsecp256k1.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eux -o pipefail 3 | 4 | ## The following script builds and installs libsecp256k1 to ~/.local/lib 5 | 6 | INSTALL_VERSION=0.5.1 7 | 8 | if [[ "$(uname -s)" =~ ^MSYS_NT.* ]]; then 9 | echo "This script is only meant to run on Windows under MSYS2" 10 | exit 1 11 | fi 12 | 13 | PREFIX="$HOME/.local" 14 | curl -LO "https://github.com/bitcoin-core/secp256k1/archive/v$INSTALL_VERSION.zip" 15 | 16 | unzip "v$INSTALL_VERSION.zip" && rm "v$INSTALL_VERSION.zip" 17 | cd "secp256k1-$INSTALL_VERSION" 18 | 19 | ./autogen.sh 20 | # hevm needs reecovery module 21 | # enable pic so static library can link against dynamic correctly 22 | ./configure --prefix=$PREFIX --enable-module-recovery --disable-benchmark --disable-tests --with-pic 23 | 24 | make install 25 | 26 | # Delete file that causes failure to link 27 | find "$PREFIX" -name libsecp256k1.dll.a -delete 28 | -------------------------------------------------------------------------------- /.github/scripts/windows-ghc-toolchain.cmake: -------------------------------------------------------------------------------- 1 | set(CMAKE_SYSTEM_NAME Windows) 2 | 3 | set(CMAKE_C_COMPILER "$ENV{HASKELL_MINGW_PATH}/bin/cc.exe") 4 | set(CMAKE_CXX_COMPILER "$ENV{HASKELL_MINGW_PATH}/bin/c++.exe") 5 | 6 | set(CMAKE_C_FLAGS "-idirafter $ENV{SYSTEM_MINGW_PATH}/include") 7 | set(CMAKE_CXX_FLAGS "-idirafter $ENV{SYSTEM_MINGW_PATH}/include") 8 | 9 | set(CMAKE_FIND_ROOT_PATH "$ENV{HASKELL_MINGW_PATH}") 10 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM BOTH) 11 | set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH) 12 | set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE BOTH) 13 | -------------------------------------------------------------------------------- /.github/workflows/bitwuzla-windows.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | 4 | env: 5 | BITWUZLA_VERSION: 0.6.1 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Cache Bitwuzla build 13 | id: cache-bitwuzla 14 | uses: actions/cache@v4 15 | with: 16 | path: bitwuzla/build/install 17 | key: ${{ runner.os }}-bitwuzla-install-${{ env.BITWUZLA_VERSION }} 18 | 19 | - name: Install Packages 20 | if: steps.cache-bitwuzla.outputs.cache-hit != 'true' 21 | run: | 22 | sudo apt-get update 23 | sudo apt-get install -y libgmp-dev ninja-build mingw-w64 24 | sudo update-alternatives --set x86_64-w64-mingw32-gcc /usr/bin/x86_64-w64-mingw32-gcc-posix 25 | sudo update-alternatives --set x86_64-w64-mingw32-g++ /usr/bin/x86_64-w64-mingw32-g++-posix 26 | 27 | - name: Setup Python Environment 28 | if: steps.cache-bitwuzla.outputs.cache-hit != 'true' 29 | run: | 30 | python3 -m venv ~/.bitwuzla-venv 31 | source ~/.bitwuzla-venv/bin/activate 32 | python3 -m pip install meson pytest cython>=3.* 33 | echo "$HOME/.bitwuzla-venv/bin/" >> $GITHUB_PATH 34 | 35 | - name: Checkout 36 | uses: actions/checkout@v4 37 | if: steps.cache-bitwuzla.outputs.cache-hit != 'true' 38 | with: 39 | repository: bitwuzla/bitwuzla 40 | ref: ${{ env.BITWUZLA_VERSION }} 41 | path: bitwuzla 42 | submodules: "recursive" 43 | persist-credentials: false 44 | 45 | - name: Wrap 46 | if: steps.cache-bitwuzla.outputs.cache-hit != 'true' 47 | run: meson wrap install gtest 48 | working-directory: bitwuzla 49 | 50 | - name: Configure 51 | if: steps.cache-bitwuzla.outputs.cache-hit != 'true' 52 | run: ./configure.py --prefix=${{ github.workspace }}/bitwuzla/build/install --win64 53 | working-directory: bitwuzla 54 | 55 | - name: Build 56 | if: steps.cache-bitwuzla.outputs.cache-hit != 'true' 57 | run: ninja install 58 | working-directory: bitwuzla/build 59 | 60 | - name: Upload Bitwuzla binary 61 | uses: actions/upload-artifact@v4 62 | with: 63 | name: bitwuzla-win64 64 | path: bitwuzla/build/install 65 | -------------------------------------------------------------------------------- /.github/workflows/check-dependencies.yml: -------------------------------------------------------------------------------- 1 | name: "Check Dependencies" 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: '30 5 * * 1' # At 05:30 AM, only on Monday 6 | pull_request: 7 | paths: 8 | - '.github/workflows/check-dependencies.yml' 9 | - '.github/scripts/**' 10 | - 'flake.*' 11 | jobs: 12 | check: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | persist-credentials: false 18 | - name: Install Nix 19 | uses: nixbuild/nix-quick-install-action@v30 20 | - name: lookup nix versions 21 | id: nixpkgs 22 | run: | 23 | NIXPKGS_REV="$(jq -r '.nodes.nixpkgs_2.locked.rev' < flake.lock)" 24 | VERSIONS="$(nix eval -I "nixpkgs=https://github.com/NixOS/nixpkgs/archive/$NIXPKGS_REV.tar.gz" --impure --json \ 25 | --expr 'let pkgs = (import {}); in { "secp256k1" = pkgs.secp256k1.version; "ff" = pkgs.libff.version; "bitwuzla" = pkgs.bitwuzla.version; }')" 26 | LIBFF_REV="$(jq .ff -r <<<"$VERSIONS")" 27 | LIBSECP256K1_REV="$(jq .secp256k1 -r <<<"$VERSIONS")" 28 | BITWUZLA_REV="$(jq .bitwuzla -r <<<"$VERSIONS")" 29 | { echo "nixpkgs=$NIXPKGS_REV"; echo "libff=$LIBFF_REV"; echo "libsecp256k1=$LIBSECP256K1_REV"; echo "bitwuzla=$BITWUZLA_REV"; } >> "$GITHUB_OUTPUT" 30 | - name: lookup local versions 31 | id: local 32 | run: | 33 | LIBFF_REV="$(grep '^INSTALL_VERSION=' .github/scripts/install-libff.sh | cut -f2 -d=)" 34 | LIBSECP256K1_REV="$(grep '^INSTALL_VERSION=' .github/scripts/install-libsecp256k1.sh | cut -f2 -d=)" 35 | BITWUZLA_REV="$(grep 'BITWUZLA_VERSION:' .github/workflows/bitwuzla-windows.yml | cut -f2 -d: | sed 's/\s//')" 36 | { echo "libff=$LIBFF_REV"; echo "libsecp256k1=$LIBSECP256K1_REV"; echo "bitwuzla=$BITWUZLA_REV"; } >> "$GITHUB_OUTPUT" 37 | - name: compare versions 38 | run: | 39 | if [ "$LIBFF_NIXPKGS" != "$LIBFF_LOCAL" ]; then 40 | echo "libff versions do not match! nix=$LIBFF_NIXPKGS local=$LIBFF_LOCAL" 41 | exit 1 42 | fi 43 | if [ "$LIBSECP256K1_NIXPKGS" != "$LIBSECP256K1_LOCAL" ]; then 44 | echo "libsecp256k1 versions do not match! nix=$LIBSECP256K1_NIXPKGS local=$LIBSECP256K1_LOCAL" 45 | exit 1 46 | fi 47 | if [ "$BITWUZLA_NIXPKGS" != "$BITWUZLA_LOCAL" ]; then 48 | echo "bitwuzla versions do not match! nix=$BITWUZLA_NIXPKGS local=$BITWUZLA_LOCAL" 49 | exit 1 50 | fi 51 | env: 52 | LIBFF_NIXPKGS: ${{ steps.nixpkgs.outputs.libff }} 53 | LIBFF_LOCAL: ${{ steps.local.outputs.libff }} 54 | LIBSECP256K1_NIXPKGS: ${{ steps.nixpkgs.outputs.libsecp256k1 }} 55 | LIBSECP256K1_LOCAL: ${{ steps.local.outputs.libsecp256k1 }} 56 | BITWUZLA_NIXPKGS: ${{ steps.nixpkgs.outputs.bitwuzla }} 57 | BITWUZLA_LOCAL: ${{ steps.local.outputs.bitwuzla }} 58 | -------------------------------------------------------------------------------- /.github/workflows/publish-docs.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Publish Documentation 3 | on: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | build-and-deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | persist-credentials: false 14 | - uses: DeterminateSystems/nix-installer-action@main 15 | - uses: DeterminateSystems/magic-nix-cache-action@main 16 | 17 | - name: build docs 18 | run: nix develop --ignore-environment --command sh -c "cd doc && mdbook build" 19 | 20 | - name: publish docs 21 | uses: JamesIves/github-pages-deploy-action@v4.6.0 22 | with: 23 | branch: gh-pages 24 | folder: doc/book 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: "Release" 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: '30 5 * * 1' # At 05:30 AM, only on Monday 6 | push: 7 | tags: 8 | - 'release/[0-9]+.[0-9]+.[0-9]+' 9 | 10 | jobs: 11 | releaseBuilds: 12 | strategy: 13 | matrix: 14 | include: 15 | # macos-13 is x86 16 | - os: "macos-13" 17 | suffix: "x86_64-macos" 18 | # macos-14 is arm, see https://github.com/orgs/community/discussions/102846 19 | # "Workflows executed on this image will run exclusively on the 3 vCPU M1 runner" 20 | - os: "macos-14" 21 | suffix: "arm64-macos" 22 | - os: "ubuntu-latest" 23 | suffix: "x86_64-linux" 24 | name: Build binary on ${{ matrix.os }} 25 | runs-on: ${{ matrix.os }} 26 | steps: 27 | - uses: actions/checkout@v4 28 | with: 29 | persist-credentials: false 30 | - name: Install Nix 31 | uses: nixbuild/nix-quick-install-action@v30 32 | - name: build hevm 33 | run: | 34 | nix build .#redistributable --out-link hevm 35 | cp ./hevm/bin/hevm ./hevm-${{ matrix.suffix }} 36 | - uses: actions/upload-artifact@v4 37 | with: 38 | name: hevm-${{ matrix.suffix }} 39 | path: ./hevm-${{ matrix.suffix }} 40 | Upload: 41 | needs: [releaseBuilds] 42 | runs-on: ubuntu-latest 43 | steps: 44 | - uses: actions/checkout@v4 45 | with: 46 | persist-credentials: false 47 | - name: Install Nix 48 | uses: nixbuild/nix-quick-install-action@v30 49 | - name: download binaries 50 | uses: actions/download-artifact@v4 51 | with: 52 | merge-multiple: true 53 | - name: create github release & upload binaries 54 | uses: softprops/action-gh-release@v2 55 | # scheduled/manual runs should not create a release 56 | if: github.event_name == 'push' 57 | with: 58 | fail_on_unmatched_files: true 59 | files: | 60 | ./hevm-x86_64-linux 61 | ./hevm-x86_64-macos 62 | ./hevm-arm64-macos 63 | - name: prepare hackage artifacts 64 | run: | 65 | # cabal complains if we don't do this... 66 | nix develop --command bash -c "cabal update" 67 | nix develop --command bash -c "cabal sdist --builddir='$RUNNER_TEMP/packages'" 68 | nix develop --command bash -c "cabal haddock lib:hevm --builddir='$RUNNER_TEMP/docs' --haddock-for-hackage --haddock-option=--hyperlinked-source" 69 | - name: publish to hackage 70 | uses: haskell-actions/hackage-publish@v1 71 | # scheduled/manual runs should not publish anything 72 | if: github.event_name == 'push' 73 | with: 74 | hackageToken: ${{ secrets.HACKAGE_AUTH_TOKEN }} 75 | packagesPath: ${{ runner.temp }}/packages/sdist 76 | docsPath: ${{ runner.temp }}/docs 77 | publish: true 78 | 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # nix 2 | result 3 | result-* 4 | 5 | # direnv 6 | .direnv/ 7 | 8 | # cabal 9 | dist/ 10 | dist-newstyle/ 11 | .configured 12 | cabal.project.local* 13 | 14 | # debug files 15 | *.smt2 16 | *.prop 17 | *.expr 18 | -------------------------------------------------------------------------------- /.hlint.yaml: -------------------------------------------------------------------------------- 1 | # ignored / annoying rules 2 | 3 | - ignore: {name: "Use fewer imports"} 4 | - ignore: {name: "Use if"} 5 | - ignore: {name: "Redundant bracket"} 6 | - ignore: {name: "Reduce duplication"} 7 | - ignore: {name: "Redundant return"} 8 | - ignore: {name: "Use head"} 9 | - ignore: {name: "Use unwords"} 10 | - ignore: {name: "Use <$>"} 11 | - ignore: {name: "Use elem"} 12 | - ignore: {name: "Use infix"} 13 | - ignore: {name: "Use section"} 14 | - ignore: {name: "Move brackets to avoid $"} 15 | - ignore: {name: "Use zipWith"} 16 | - ignore: {name: "Redundant flip"} 17 | - ignore: {name: "Redundant $!"} 18 | - ignore: {name: "Redundant <$>"} 19 | - ignore: {name: "Eta reduce"} 20 | - ignore: {name: "Use underscore"} 21 | - ignore: {name: "Functor law"} 22 | - ignore: {name: "Use record patterns"} 23 | 24 | # custom rules 25 | 26 | # we should use witch 27 | - functions: 28 | - {name: fromIntegral, within: [], message: "use into or tryFrom from Witch"} 29 | - {name: fromInteger, within: [], message: "use into or tryFrom from Witch"} 30 | - {name: toIntegral, within: [], message: "use into or tryFrom from Witch"} 31 | - {name: toInteger, within: [], message: "use into or tryFrom from Witch"} 32 | 33 | # we should use internalError 34 | - error: {lhs: error, rhs: internalError} 35 | # we should use pure 36 | - error: {lhs: return, rhs: pure} 37 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | -- Copyright (C) 2017-2022 mbrock, dbrock, mrchico, dxo, Ethereum Foundation 2 | -------------------------------------------------------------------------------- /bench/contracts/deposit.sol: -------------------------------------------------------------------------------- 1 | /** 2 | *Submitted for verification at Etherscan.io on 2020-10-14 3 | */ 4 | 5 | // ┏━━━┓━┏┓━┏┓━━┏━━━┓━━┏━━━┓━━━━┏━━━┓━━━━━━━━━━━━━━━━━━━┏┓━━━━━┏━━━┓━━━━━━━━━┏┓━━━━━━━━━━━━━━┏┓━ 6 | // ┃┏━━┛┏┛┗┓┃┃━━┃┏━┓┃━━┃┏━┓┃━━━━┗┓┏┓┃━━━━━━━━━━━━━━━━━━┏┛┗┓━━━━┃┏━┓┃━━━━━━━━┏┛┗┓━━━━━━━━━━━━┏┛┗┓ 7 | // ┃┗━━┓┗┓┏┛┃┗━┓┗┛┏┛┃━━┃┃━┃┃━━━━━┃┃┃┃┏━━┓┏━━┓┏━━┓┏━━┓┏┓┗┓┏┛━━━━┃┃━┗┛┏━━┓┏━┓━┗┓┏┛┏━┓┏━━┓━┏━━┓┗┓┏┛ 8 | // ┃┏━━┛━┃┃━┃┏┓┃┏━┛┏┛━━┃┃━┃┃━━━━━┃┃┃┃┃┏┓┃┃┏┓┃┃┏┓┃┃━━┫┣┫━┃┃━━━━━┃┃━┏┓┃┏┓┃┃┏┓┓━┃┃━┃┏┛┗━┓┃━┃┏━┛━┃┃━ 9 | // ┃┗━━┓━┃┗┓┃┃┃┃┃┃┗━┓┏┓┃┗━┛┃━━━━┏┛┗┛┃┃┃━┫┃┗┛┃┃┗┛┃┣━━┃┃┃━┃┗┓━━━━┃┗━┛┃┃┗┛┃┃┃┃┃━┃┗┓┃┃━┃┗┛┗┓┃┗━┓━┃┗┓ 10 | // ┗━━━┛━┗━┛┗┛┗┛┗━━━┛┗┛┗━━━┛━━━━┗━━━┛┗━━┛┃┏━┛┗━━┛┗━━┛┗┛━┗━┛━━━━┗━━━┛┗━━┛┗┛┗┛━┗━┛┗┛━┗━━━┛┗━━┛━┗━┛ 11 | // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┃┃━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 12 | // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┗┛━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 13 | 14 | // SPDX-License-Identifier: CC0-1.0 15 | 16 | pragma solidity 0.6.11; 17 | 18 | // This interface is designed to be compatible with the Vyper version. 19 | /// @notice This is the Ethereum 2.0 deposit contract interface. 20 | /// For more information see the Phase 0 specification under https://github.com/ethereum/eth2.0-specs 21 | interface IDepositContract { 22 | /// @notice A processed deposit event. 23 | event DepositEvent( 24 | bytes pubkey, 25 | bytes withdrawal_credentials, 26 | bytes amount, 27 | bytes signature, 28 | bytes index 29 | ); 30 | 31 | /// @notice Submit a Phase 0 DepositData object. 32 | /// @param pubkey A BLS12-381 public key. 33 | /// @param withdrawal_credentials Commitment to a public key for withdrawals. 34 | /// @param signature A BLS12-381 signature. 35 | /// @param deposit_data_root The SHA-256 hash of the SSZ-encoded DepositData object. 36 | /// Used as a protection against malformed input. 37 | function deposit( 38 | bytes calldata pubkey, 39 | bytes calldata withdrawal_credentials, 40 | bytes calldata signature, 41 | bytes32 deposit_data_root 42 | ) external payable; 43 | 44 | /// @notice Query the current deposit root hash. 45 | /// @return The deposit root hash. 46 | function get_deposit_root() external view returns (bytes32); 47 | 48 | /// @notice Query the current deposit count. 49 | /// @return The deposit count encoded as a little endian 64-bit number. 50 | function get_deposit_count() external view returns (bytes memory); 51 | } 52 | 53 | // Based on official specification in https://eips.ethereum.org/EIPS/eip-165 54 | interface ERC165 { 55 | /// @notice Query if a contract implements an interface 56 | /// @param interfaceId The interface identifier, as specified in ERC-165 57 | /// @dev Interface identification is specified in ERC-165. This function 58 | /// uses less than 30,000 gas. 59 | /// @return `true` if the contract implements `interfaceId` and 60 | /// `interfaceId` is not 0xffffffff, `false` otherwise 61 | function supportsInterface(bytes4 interfaceId) external pure returns (bool); 62 | } 63 | 64 | // This is a rewrite of the Vyper Eth2.0 deposit contract in Solidity. 65 | // It tries to stay as close as possible to the original source code. 66 | /// @notice This is the Ethereum 2.0 deposit contract interface. 67 | /// For more information see the Phase 0 specification under https://github.com/ethereum/eth2.0-specs 68 | contract DepositContract is IDepositContract, ERC165 { 69 | uint constant DEPOSIT_CONTRACT_TREE_DEPTH = 32; 70 | // NOTE: this also ensures `deposit_count` will fit into 64-bits 71 | uint constant MAX_DEPOSIT_COUNT = 2**DEPOSIT_CONTRACT_TREE_DEPTH - 1; 72 | 73 | bytes32[DEPOSIT_CONTRACT_TREE_DEPTH] branch; 74 | uint256 deposit_count; 75 | 76 | bytes32[DEPOSIT_CONTRACT_TREE_DEPTH] zero_hashes; 77 | 78 | constructor() public { 79 | // Compute hashes in empty sparse Merkle tree 80 | for (uint height = 0; height < DEPOSIT_CONTRACT_TREE_DEPTH - 1; height++) 81 | zero_hashes[height + 1] = sha256(abi.encodePacked(zero_hashes[height], zero_hashes[height])); 82 | } 83 | 84 | function get_deposit_root() override external view returns (bytes32) { 85 | bytes32 node; 86 | uint size = deposit_count; 87 | for (uint height = 0; height < DEPOSIT_CONTRACT_TREE_DEPTH; height++) { 88 | if ((size & 1) == 1) 89 | node = sha256(abi.encodePacked(branch[height], node)); 90 | else 91 | node = sha256(abi.encodePacked(node, zero_hashes[height])); 92 | size /= 2; 93 | } 94 | return sha256(abi.encodePacked( 95 | node, 96 | to_little_endian_64(uint64(deposit_count)), 97 | bytes24(0) 98 | )); 99 | } 100 | 101 | function get_deposit_count() override external view returns (bytes memory) { 102 | return to_little_endian_64(uint64(deposit_count)); 103 | } 104 | 105 | function deposit( 106 | bytes calldata pubkey, 107 | bytes calldata withdrawal_credentials, 108 | bytes calldata signature, 109 | bytes32 deposit_data_root 110 | ) override external payable { 111 | // Extended ABI length checks since dynamic types are used. 112 | require(pubkey.length == 48, "DepositContract: invalid pubkey length"); 113 | require(withdrawal_credentials.length == 32, "DepositContract: invalid withdrawal_credentials length"); 114 | require(signature.length == 96, "DepositContract: invalid signature length"); 115 | 116 | // Check deposit amount 117 | require(msg.value >= 1 ether, "DepositContract: deposit value too low"); 118 | require(msg.value % 1 gwei == 0, "DepositContract: deposit value not multiple of gwei"); 119 | uint deposit_amount = msg.value / 1 gwei; 120 | require(deposit_amount <= type(uint64).max, "DepositContract: deposit value too high"); 121 | 122 | // Emit `DepositEvent` log 123 | bytes memory amount = to_little_endian_64(uint64(deposit_amount)); 124 | emit DepositEvent( 125 | pubkey, 126 | withdrawal_credentials, 127 | amount, 128 | signature, 129 | to_little_endian_64(uint64(deposit_count)) 130 | ); 131 | 132 | // Compute deposit data root (`DepositData` hash tree root) 133 | bytes32 pubkey_root = sha256(abi.encodePacked(pubkey, bytes16(0))); 134 | bytes32 signature_root = sha256(abi.encodePacked( 135 | sha256(abi.encodePacked(signature[:64])), 136 | sha256(abi.encodePacked(signature[64:], bytes32(0))) 137 | )); 138 | bytes32 node = sha256(abi.encodePacked( 139 | sha256(abi.encodePacked(pubkey_root, withdrawal_credentials)), 140 | sha256(abi.encodePacked(amount, bytes24(0), signature_root)) 141 | )); 142 | 143 | // Verify computed and expected deposit data roots match 144 | require(node == deposit_data_root, "DepositContract: reconstructed DepositData does not match supplied deposit_data_root"); 145 | 146 | // Avoid overflowing the Merkle tree (and prevent edge case in computing `branch`) 147 | require(deposit_count < MAX_DEPOSIT_COUNT, "DepositContract: merkle tree full"); 148 | 149 | // Add deposit data root to Merkle tree (update a single `branch` node) 150 | deposit_count += 1; 151 | uint size = deposit_count; 152 | for (uint height = 0; height < DEPOSIT_CONTRACT_TREE_DEPTH; height++) { 153 | if ((size & 1) == 1) { 154 | branch[height] = node; 155 | return; 156 | } 157 | node = sha256(abi.encodePacked(branch[height], node)); 158 | size /= 2; 159 | } 160 | // As the loop should always end prematurely with the `return` statement, 161 | // this code should be unreachable. We assert `false` just to be safe. 162 | assert(false); 163 | } 164 | 165 | function supportsInterface(bytes4 interfaceId) override external pure returns (bool) { 166 | return interfaceId == type(ERC165).interfaceId || interfaceId == type(IDepositContract).interfaceId; 167 | } 168 | 169 | function to_little_endian_64(uint64 value) internal pure returns (bytes memory ret) { 170 | ret = new bytes(8); 171 | bytes8 bytesValue = bytes8(value); 172 | // Byteswapping during copying to bytes. 173 | ret[0] = bytesValue[7]; 174 | ret[1] = bytesValue[6]; 175 | ret[2] = bytesValue[5]; 176 | ret[3] = bytesValue[4]; 177 | ret[4] = bytesValue[3]; 178 | ret[5] = bytesValue[2]; 179 | ret[6] = bytesValue[1]; 180 | ret[7] = bytesValue[0]; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /bench/contracts/erc20.sol: -------------------------------------------------------------------------------- 1 | contract ERC20 { 2 | // --- ERC20 Data --- 3 | string public constant name = "TOKEN"; 4 | string public constant symbol = "TKN"; 5 | string public constant version = "1"; 6 | uint8 public constant decimals = 18; 7 | uint256 public totalSupply; 8 | 9 | mapping (address => uint) public balanceOf; 10 | mapping (address => mapping (address => uint)) public allowance; 11 | 12 | event Approval(address indexed src, address indexed guy, uint wad); 13 | event Transfer(address indexed src, address indexed dst, uint wad); 14 | 15 | constructor(uint supply) public { 16 | balanceOf[msg.sender] = supply; 17 | totalSupply = supply; 18 | } 19 | 20 | // --- Token --- 21 | function transfer(address dst, uint wad) external returns (bool) { 22 | return transferFrom(msg.sender, dst, wad); 23 | } 24 | function transferFrom(address src, address dst, uint wad) 25 | public returns (bool) 26 | { 27 | require(balanceOf[src] >= wad, "erc20/insufficient-balance"); 28 | if (src != msg.sender && allowance[src][msg.sender] != type(uint).max) { 29 | require(allowance[src][msg.sender] >= wad, "erc20/insufficient-allowance"); 30 | allowance[src][msg.sender] -= wad; 31 | } 32 | balanceOf[src] -= wad; 33 | balanceOf[dst] += wad; 34 | emit Transfer(src, dst, wad); 35 | return true; 36 | } 37 | function approve(address usr, uint wad) external returns (bool) { 38 | allowance[msg.sender][usr] = wad; 39 | emit Approval(msg.sender, usr, wad); 40 | return true; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /doc/.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | node_modules 3 | dist 4 | .parcel-cache 5 | -------------------------------------------------------------------------------- /doc/.prettierignore: -------------------------------------------------------------------------------- 1 | *.min.js 2 | *.html 3 | *.css 4 | *.json -------------------------------------------------------------------------------- /doc/.prettierrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // "schema": "https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/prettierrc.json", 3 | // "$schema": "http://json.schemastore.org/prettierrc", 4 | module.exports = { 5 | arrowParens: 'always', 6 | bracketSpacing: true, 7 | endOfLine: 'lf', 8 | printWidth: 100, 9 | proseWrap: 'never', 10 | singleQuote: true, 11 | tabWidth: 2, 12 | trailingComma: 'all', 13 | quoteProps: 'as-needed', 14 | embeddedLanguageFormatting: 'auto', 15 | semi: true, 16 | overrides: [ 17 | { 18 | files: '*.md', 19 | options: { 20 | parser: 'markdown', 21 | printWidth: 80, 22 | proseWrap: 'always', 23 | tabWidth: 4, 24 | useTabs: true, 25 | singleQuote: false, 26 | bracketSpacing: true, 27 | }, 28 | }, 29 | ], 30 | }; 31 | -------------------------------------------------------------------------------- /doc/book.css: -------------------------------------------------------------------------------- 1 | table { 2 | margin: 0 auto; 3 | border-collapse: collapse; 4 | width: 100%; 5 | } 6 | 7 | table td:first-child { 8 | width: 15%; 9 | } 10 | 11 | table td:nth-child(2) { 12 | width: 25%; 13 | } -------------------------------------------------------------------------------- /doc/book.toml: -------------------------------------------------------------------------------- 1 | [preprocessor.mermaid] 2 | command = "mdbook-mermaid" 3 | 4 | [book] 5 | authors = ["dxo", "msoos"] 6 | language = "en" 7 | multilingual = false 8 | src = "src" 9 | title = "hevm" 10 | 11 | 12 | [output.html] 13 | no-section-label = true 14 | additional-js = ["solidity.min.js", "mermaid.min.js", "mermaid-init.js"] 15 | additional-css = ["book.css"] 16 | git-repository-url = "https://github.com/ethereum/hevm" 17 | edit-url-template = "hhttps://github.com/ethereum/hevm/edit/main/{path}" 18 | 19 | git-repository-icon = "fa-github" 20 | 21 | [output.html.fold] 22 | enable = true 23 | -------------------------------------------------------------------------------- /doc/cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Bufs", 4 | "dapptools", 5 | "endstates", 6 | "Halmos", 7 | "Hevm", 8 | "Kontrol", 9 | "preconfigured", 10 | "reentrancy", 11 | "returndata", 12 | "txdata" 13 | ] 14 | } -------------------------------------------------------------------------------- /doc/mermaid-init.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | const darkThemes = ['ayu', 'navy', 'coal']; 3 | const lightThemes = ['light', 'rust']; 4 | 5 | const classList = document.getElementsByTagName('html')[0].classList; 6 | 7 | let lastThemeWasLight = true; 8 | for (const cssClass of classList) { 9 | if (darkThemes.includes(cssClass)) { 10 | lastThemeWasLight = false; 11 | break; 12 | } 13 | } 14 | 15 | const theme = lastThemeWasLight ? 'default' : 'dark'; 16 | mermaid.initialize({ startOnLoad: true, theme }); 17 | 18 | // Simplest way to make mermaid re-render the diagrams in the new theme is via refreshing the page 19 | 20 | for (const darkTheme of darkThemes) { 21 | document.getElementById(darkTheme).addEventListener('click', () => { 22 | if (lastThemeWasLight) { 23 | window.location.reload(); 24 | } 25 | }); 26 | } 27 | 28 | for (const lightTheme of lightThemes) { 29 | document.getElementById(lightTheme).addEventListener('click', () => { 30 | if (!lastThemeWasLight) { 31 | window.location.reload(); 32 | } 33 | }); 34 | } 35 | })(); 36 | -------------------------------------------------------------------------------- /doc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hevm-book", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "parcel build theme/theme.js --no-source-maps && cp dist/theme.js theme/highlight.js", 7 | "fmt:docs": "npx prettier@next --config .prettierrc.js --write '**/*.md' --no-cache" 8 | }, 9 | "devDependencies": { 10 | "parcel": "^2.8.1", 11 | "prettier": "^4.0.0-alpha.8" 12 | }, 13 | "dependencies": { 14 | "highlight.js": "^10.7.3", 15 | "highlightjs-solidity": "^2.0.5" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /doc/readme.md: -------------------------------------------------------------------------------- 1 | # hevm Book 2 | 3 | The hevm book is built using `mdbook` and `yarn`. Known good versions of both 4 | are included in the flake `devShell`. 5 | 6 | ## Running the mdBook server 7 | 8 | You can then serve the documentation locally by calling the [`serve` 9 | command][mdbook-serve] from the `book` directory: 10 | 11 | ```shell 12 | mdbook serve 13 | ``` 14 | 15 | Alternatively it can be called from the root of the repository: 16 | 17 | ```shell 18 | mdbook serve doc 19 | ``` 20 | 21 | [mdbook-serve]: https://rust-lang.github.io/mdBook/cli/serve.html 22 | 23 | ## Updating syntax highlighting 24 | 25 | In order to highlight the Solidity code examples in the book we override 26 | mdBook's built-in [highlight.js] with our own. To update the syntax highlighting, 27 | run the following commands using [yarn]: 28 | 29 | ```shell 30 | yarn install 31 | yarn build 32 | ``` 33 | 34 | This will build `theme/theme.js` and copy a minified version of the output into 35 | `theme/highlight.js` where it will be picked up by mdbook during build. You 36 | should not need to do this unless you want to modify syntax highlighting in some 37 | way. 38 | 39 | [highlight.js]: https://highlightjs.org/ 40 | [Yarn]: (https://yarnpkg.com/) 41 | -------------------------------------------------------------------------------- /doc/solidity.min.js: -------------------------------------------------------------------------------- 1 | hljs.registerLanguage("solidity",(()=>{"use strict";function e(){try{return!0 2 | }catch(e){return!1}} 3 | var a=/-?(\b0[xX]([a-fA-F0-9]_?)*[a-fA-F0-9]|(\b[1-9](_?\d)*(\.((\d_?)*\d)?)?|\.\d(_?\d)*)([eE][-+]?\d(_?\d)*)?|\b0)(?!\w|\$)/ 4 | ;e()&&(a=a.source.replace(/\\b/g,"(?{ 14 | var a=r(e),o=l(e),c=/[A-Za-z_$][A-Za-z_$0-9.]*/,d=e.inherit(e.TITLE_MODE,{ 15 | begin:/[A-Za-z$_][0-9A-Za-z$_]*/,lexemes:c,keywords:n}),u={className:"params", 16 | begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,lexemes:c,keywords:n, 17 | contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,o,s]},_={ 18 | className:"operator",begin:/:=|->/};return{keywords:n,lexemes:c, 19 | contains:[a,o,i,t,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,s,_,{ 20 | className:"function",lexemes:c,beginKeywords:"function",end:"{",excludeEnd:!0, 21 | contains:[d,u,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,_]}]}}, 22 | solAposStringMode:r,solQuoteStringMode:l,HEX_APOS_STRING_MODE:i, 23 | HEX_QUOTE_STRING_MODE:t,SOL_NUMBER:s,isNegativeLookbehindAvailable:e} 24 | ;const{baseAssembly:c,solAposStringMode:d,solQuoteStringMode:u,HEX_APOS_STRING_MODE:_,HEX_QUOTE_STRING_MODE:m,SOL_NUMBER:b,isNegativeLookbehindAvailable:E}=o 25 | ;return e=>{for(var a=d(e),s=u(e),n=[],i=0;i<32;i++)n[i]=i+1 26 | ;var t=n.map((e=>8*e)),r=[];for(i=0;i<=80;i++)r[i]=i 27 | ;var l=n.map((e=>"bytes"+e)).join(" ")+" ",o=t.map((e=>"uint"+e)).join(" ")+" ",g=t.map((e=>"int"+e)).join(" ")+" ",M=[].concat.apply([],t.map((e=>r.map((a=>e+"x"+a))))),p={ 28 | keyword:"var bool string int uint "+g+o+"byte bytes "+l+"fixed ufixed "+M.map((e=>"fixed"+e)).join(" ")+" "+M.map((e=>"ufixed"+e)).join(" ")+" enum struct mapping address new delete if else for while continue break return throw emit try catch revert unchecked _ function modifier event constructor fallback receive error virtual override constant immutable anonymous indexed storage memory calldata external public internal payable pure view private returns import from as using pragma contract interface library is abstract type assembly", 29 | literal:"true false wei gwei szabo finney ether seconds minutes hours days weeks years", 30 | built_in:"self this super selfdestruct suicide now msg block tx abi blockhash gasleft assert require Error Panic sha3 sha256 keccak256 ripemd160 ecrecover addmod mulmod log0 log1 log2 log3 log4" 31 | },O={className:"operator",begin:/[+\-!~*\/%<>&^|=]/ 32 | },C=/[A-Za-z_$][A-Za-z_$0-9]*/,N={className:"params",begin:/\(/,end:/\)/, 33 | excludeBegin:!0,excludeEnd:!0,lexemes:C,keywords:p, 34 | contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,a,s,b,"self"]},f={ 35 | begin:/\.\s*/,end:/[^A-Za-z0-9$_\.]/,excludeBegin:!0,excludeEnd:!0,keywords:{ 36 | built_in:"gas value selector address length push pop send transfer call callcode delegatecall staticcall balance code codehash wrap unwrap name creationCode runtimeCode interfaceId min max" 37 | },relevance:2},y=e.inherit(e.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/, 38 | lexemes:C,keywords:p}),w={className:"built_in", 39 | begin:(E()?"(? 12 | 13 | # Reference 14 | 15 | - [Forge test proving](./test.md) 16 | - [Symbolic unit execution](./symbolic.md) 17 | - [Equivalence checking](./equivalence.md) 18 | - [Concrete execution](./exec.md) 19 | - [Common options](./common-options.md) 20 | 21 | # Development 22 | 23 | - [Developers tutorial](./for-hevm-developers.md) 24 | - [System architecture](./architecture.md) 25 | -------------------------------------------------------------------------------- /doc/src/architecture.md: -------------------------------------------------------------------------------- 1 | # Steppers & Interpreters 2 | The core EVM semantics in hevm can be found in `EVM.hs`. EVM state is contained 3 | in the `VM` record, and the `exec1` function executes a single opcode inside 4 | the monad `type EVM a = State VM a`. 5 | 6 | The core semantics are pure, and should information from the outside world be 7 | required to continue execution (RPC queries or SMT queries), 8 | execution will halt, and the `result` field of the VM will be an instance of 9 | `VMFailure (Query _)`. 10 | 11 | Multiple steps of EVM execution are orchestrated via interpreters for a meta 12 | language. Programs in the meta language are called Steppers. The instructions 13 | in the meta language can be found in `Stepper.hs`. 14 | 15 | There can potentially be many different interpreters with different features. Currently, 16 | we provide a concrete and a symbolic interpreter. Interpreters can 17 | handle Queries in different ways, for example in the symbolic interpreter, both 18 | sides of a branch point will be explored, while in the concrete interpreter, 19 | such branching is not permitted. 20 | 21 | Interpreters are parametrized by a `Fetcher` that can handle RPC and SMT 22 | queries, and can be instantiated with fetchers that could have different 23 | fetching strategies (e.g. caching). Interpreters execute Steppers and use their 24 | Fetcher to handle any Queries that need to be resolved. 25 | 26 | This architecture is very modular and pluggable, and allows the core semantics 27 | to be shared between different interpreters, as well as the reuse of steppers 28 | between different interpreters, making it easy to e.g. share the same test 29 | execution strategy between concrete and symbolic interpreters. 30 | ```mermaid 31 | graph LR 32 | subgraph meta-language 33 | A[Stepper] 34 | end 35 | subgraph interpreters 36 | A --> B[Concrete] 37 | A --> C[Symbolic] 38 | end 39 | subgraph fetchers 40 | F[Fetch.hs] 41 | B --> F 42 | C --> F 43 | end 44 | subgraph EVM Semantics 45 | G[EVM.hs] 46 | B --> G 47 | C --> G 48 | end 49 | ``` 50 | 51 | # Expr 52 | The symbolic execution features in hevm are built using a custom IR, 53 | imaginatively named `Expr`. This is a summarized trace semantics of a given 54 | EVM program. 55 | 56 | One important principle is that of local context: e.g. each term representing a 57 | read from a Buf/Storage will always contain a snapshot of the state of the 58 | buffer/store at the time the read occurred. This ensures that all context 59 | relevant to a given operation is contained within the term that represents that 60 | operation, and allows subsequent analysis to be stateless. 61 | 62 | Expressions in this language can have the following types: 63 | - `End`: control flow 64 | - `Word`: a 256 bit word (a stack item) 65 | - `Byte`: a single byte 66 | - `Buf`: a byte array (used for calldata, memory and returndata) 67 | - `Storage`: contract storage 68 | - `Logs`: EVM logs 69 | 70 | ## Control Flow 71 | An EVM program is represented by an `Expr End`, which is either a single end 72 | state for a program without branches, or a series of nested if-then-else terms, 73 | where each leaf is an end state. Some end states (e.g. `Return`) contain copies 74 | of any externally observable data (i.e. returndata and post call storage). 75 | 76 | As an example the following Expr encodes a program that branches based on the 77 | equality of two symbolic words ("a" and "b"), and returns if they are equal and 78 | reverts if they are not: 79 | ```haskell 80 | (ITE (Eq (Var "a") (Var "b")) (Success ...) (Failure ...)) 81 | ``` 82 | 83 | ## Buffers 84 | Memory, calldata, and returndata are all represented as a Buf. Semantically 85 | speaking a Buf is a byte array with of size 2^256. 86 | 87 | Bufs have three base constructors: 88 | - AbstractBuf: all elements are fully abstract values 89 | - ConcreteBuf bs: all elements past (length bs) are zero 90 | 91 | Bufs can be read from with: 92 | - ReadByte idx buf: read the byte at idx from buf 93 | - ReadWord idx buf: read the byte at idx from buf 94 | 95 | Bufs can be written to with: 96 | - WriteByte idx val buf: write val to idx in buf 97 | - WriteWord idx val buf: write val to idx in buf 98 | - CopySlice srcOffset dstOffset size src dst: 99 | overwrite dstOffset -> dstOffset + size in dst with srcOffset -> srcOffset + size from src 100 | 101 | e.g. the following Buf expression represents an abi encoded call to `foo(uint256 a)`: 102 | ```haskell 103 | (WriteWord (Lit 0x4) (Var "a") 104 | (WriteByte (Lit 0x3) (LitByte 56) 105 | (WriteByte (Lit 0x2) (LitByte 189) 106 | (WriteByte (Lit 0x1) (LitByte 190) 107 | (WriteByte (Lit 0x0) (LitByte 47) 108 | (AbstractBuf "txdata"))))) 109 | ``` 110 | 111 | This represents calldata of the form: 112 | ``` 113 | ----------------------------------------------------------------------- 114 | | | | arbitrary symbolic data.... | 115 | ----------------------------------------------------------------------- 116 | ``` 117 | 118 | Note that a Buf expression contains a copy of all historical writes, meaning 119 | that it is possible to write multiple times to the same location. In this case 120 | only the topmost write is relevant. This allows us to mix symbolic and concrete 121 | writes to the same buffer. 122 | 123 | ## Storage 124 | Storage expressions are similar, but instead of writing regions of bytes, we 125 | write a word to a particular key in a given addresses storage. Note that as 126 | with a Buf, writes can be sequenced on top of concrete, empty and fully 127 | abstract starting states. 128 | 129 | As with Bufs, Storage expressions contain a full history of all previous writes. 130 | 131 | For example the following expression represents a write of a symbolic word "c" 132 | to slot 2 for the zero address followed by a write of 1 to the slot at the 133 | symbolic location "b" for the zero address. These writes are sequenced on top 134 | of an `EmptyStore` meaning all other storage locations are held to be 0. 135 | ```haskell 136 | (SStore (Lit 0) (Var "b") (Lit 1) 137 | (SStore (Lit 0) (Lit 2) (Var "c") 138 | EmptyStore)) 139 | ``` 140 | 141 | ## Logs 142 | Logs are also represented as a sequence of writes, but unlike Buf and Storage 143 | expressions, Log writes are always sequenced on an empty starting point, and 144 | overwriting is not allowed. 145 | 146 | # Symbolic Execution 147 | During symbolic execution all possible branches of the program are explored 148 | symbolically. Reachability analysis is performed at this stage only if needed 149 | for loop unrolling. This produces an Expr End. As an example consider the 150 | following program: 151 | ```solidity 152 | contract MyContract { 153 | mapping(uint => uint) items; 154 | function test(uint val1) public { 155 | require(val1 > 10); 156 | unchecked { 157 | items[4] = val1+1; 158 | assert(items[4] > 10); 159 | } 160 | } 161 | } 162 | ``` 163 | 164 | This decompiles into the following Expr End: 165 | ![Body frame](expr.png) 166 | 167 | For more details, see our research paper on hevm on open access 168 | [research 169 | paper](https://link.springer.com/content/pdf/10.1007/978-3-031-65627-9_22.pdf?pdf=inline+link) 170 | as presented at CAV 2024, [presentation 171 | here](https://github.com/msooseth/hevm-presentation/blob/main/HEVM-presentation%20CAV%2026th%20July%202024.pdf) 172 | -------------------------------------------------------------------------------- /doc/src/common-options.md: -------------------------------------------------------------------------------- 1 | # Common options 2 | 3 | The subcommands of hevm present a number 4 | of common options. Here, we document these options in detail. 5 | 6 | ## Maximum Buffer Size, ``--max-buf-size`` 7 | 8 | The buffers in hevm are limited to a maximum size of 2^N bytes, where N is by 9 | default 64, but adjustable via the `--max-buf-size` flag. This helps to prevent 10 | the system from creating buffers that are too large and would exceed the gas 11 | limit. Limiting this value further to e.g. 20 can help to force the system to 12 | generate counterexamples that are easier to examine and understand. 13 | 14 | ## Choice of Solver, ``--solver`` 15 | 16 | hevm can use any SMT solver that supports the AUFBV theory and incremental 17 | solving. Currently, z3, cvc5, and bitwuzla's interfaces are implemented. While 18 | any of these solvers work, we recommend using bitwuzla as it is in general 19 | extremely fast, almost always significantly faster than e.g. z3. 20 | 21 | ## Number of Solvers, ``--num-solvers`` 22 | 23 | hevm can run multiple solvers in parallel and will run as many solvers as it 24 | detects the number of CPU cores on the machine. However, in some cases, that 25 | may lead to memory outs, in case the solver happens to get queries that are 26 | memory-intensive. In these cases, the number of solvers can be limited to a a 27 | specific (low) number via the `--num-solvers` flag. 28 | 29 | ## Promising no reentrancy, ``--promise-no-reent`` 30 | 31 | hevm can be instructed to assume that no reentrancy will occur during the 32 | execution of the contract. This is currently neccessary to fully explore 33 | certain contracts. This is because value transfer is usually done via a `CALL`, 34 | which can be reentrant. By promising no reentrancy, the system can assume that 35 | no reentrancy will occur and can explore the contract more fully. 36 | 37 | ## Timeout for SMT queries, ``--smttimeout`` 38 | 39 | Some queries take too long. With a timeout, we ensure that hevm eventually 40 | terminates. However, endstates where the timeout was reached are considered 41 | inditerminate, and will lead to a `WARNING` in the output. It is worthwhile 42 | trying to switch to a different SMT solver such as bitwuzla, or increasing the 43 | timeout if this happens. 44 | 45 | ## Loop Iteration Limit, ``--ask-smt-iterations`` 46 | 47 | Loops in the code cause a challenge to symbolic execution framework. In order 48 | to not run indefinitely, hevm will only explore a certain number of iterations 49 | of a loop before consideing abandoning the exploration of that branch. This 50 | number can be set via the `--ask-smt-iterations` flag. 51 | 52 | ## Maximum Branch Width Limit, ``--max-width`` 53 | 54 | Limits the number of potential concrete values that are explored in case a 55 | symbolic value is encountered, thus limiting branching width. For example, if a 56 | JUMP instruction is called with a symbolic expression, the system will explore 57 | all possible valid jump destinations, which may be too many. This option limits 58 | the branching factor in these cases. Default is 100. 59 | 60 | If there are more than the given maximum number of possible values, the system 61 | will try to deal with the symbolic value, if possible, e.g. via 62 | over-approximation. If over-approximation is not possible, symbolic execution 63 | will terminate with a `Partial` node, which is often displayed as "Unexpected 64 | Symbolic Arguments to Opcode" to the user when e.g. running `hevm test`. 65 | 66 | ## Maximum Branch Depth Limit, ``--max-depth`` 67 | 68 | Limits the number of branching points on all paths during symbolic execution. 69 | This is helpful to prevent the exploration from running for too long. Useful in 70 | scenarios where you use e.g. both symbolic execution and fuzzing, and don't 71 | want the symbolic execution to run for too long. It will often read to 72 | WARNING-s related to `Branches too deep at program counter`. 73 | -------------------------------------------------------------------------------- /doc/src/equivalence-checking-tutorial.md: -------------------------------------------------------------------------------- 1 | # Equivalence Checking Tutorial 2 | 3 | Equivalence checking allows to check whether two bytecodes do the same thing under all input 4 | circumstances. This allows to e.g. create two functions, one that is known to be good, and 5 | another that uses less gas, but is hard to check for correctness. Then, with equivalence 6 | checking, one can check whether the two behave the same. 7 | 8 | The notion of equivalence in hevm is defined as follows. Two contracts are equivalent 9 | if for all possible calldata and state, after execution has finished, their observable 10 | storage state is equivalent and they return the same value. In particular, the 11 | following is NOT checked when checking for equivalence: 12 | - [Gas](https://ethereum.org/en/developers/docs/gas/) consumption 13 | - [Events](https://solidity-by-example.org/events/) emitted 14 | - Maximum stack depth 15 | - Maximum memory usage 16 | 17 | Note that in the Solidity ABI, the calldata's first 4 bytes are the 18 | [function selector](https://docs.soliditylang.org/en/latest/abi-spec.html#function-selector) 19 | which decide which function is being called, along with the potential 20 | [fallback](https://solidity-by-example.org/fallback/) function mechanism. 21 | Hence, treating calldata as symbolic covers all possible function calls, 22 | including fallback functions. While not all contracts 23 | [follow the Solidity ABI](https://github.com/ethereum/requests-for-proposals/blob/master/open-rfps/pectra-system-contracts-audit.md), 24 | since hevm's symbolic equivalence checker does not distinguish between function 25 | selector and function parameter bytes in the calldata, it will still correctly 26 | check the equivalence of such non-conforming contracts. 27 | 28 | ## Finding Discrepancies 29 | Let's see this toy contract, in file contract1.sol: 30 | ```solidity 31 | //SPDX-License-Identifier: MIT 32 | pragma solidity ^0.8.19; 33 | contract MyContract { 34 | mapping (address => uint) balances; 35 | function my_adder(address recv, uint amt) public { 36 | if (balances[recv] + amt >= 100) { revert(); } 37 | balances[recv] += amt; 38 | } 39 | } 40 | ``` 41 | 42 | And this, slightly modified one, in file contract2.sol: 43 | ```solidity 44 | //SPDX-License-Identifier: MIT 45 | pragma solidity ^0.8.19; 46 | contract MyContract { 47 | mapping (address => uint) balances; 48 | function my_adder(address recv, uint amt) public { 49 | if (balances[recv] + amt >= 100) { revert(); } 50 | balances[recv] += amt/2; 51 | balances[recv] += amt/2; 52 | } 53 | } 54 | ``` 55 | 56 | Let's ask hevm to compare the two: 57 | ```shell 58 | $ hevm equivalence \ 59 | --code-a $(solc --bin-runtime "contract1.sol" | tail -n1) \ 60 | --code-b $(solc --bin-runtime "contract2.sol" | tail -n1) 61 | Found 90 total pairs of endstates 62 | Asking the SMT solver for 58 pairs 63 | Reuse of previous queries was Useful in 0 cases 64 | Not equivalent. The following inputs result in differing behaviours: 65 | ----- 66 | Calldata: 67 | 0xafc2c94900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000023 68 | Storage: 69 | Addr SymAddr "entrypoint": [(0x0,0x10)] 70 | Transaction Context: 71 | TxValue: 0x0 72 | ``` 73 | 74 | This tells us that with a value of 0x23 being sent, which corresponds 75 | to 35, the two are not equivalent. This is indeed the case: one will add `35 76 | div 2 = 17` twice, which is 34, the other will add 35. 77 | 78 | ## Fixing and Proving Correctness 79 | Let's fix the above issue by incrementing the balance by 1 in case it's an odd 80 | value. Let's call this contract3.sol: 81 | ```solidity 82 | //SPDX-License-Identifier: MIT 83 | pragma solidity ^0.8.19; 84 | contract MyContract { 85 | mapping (address => uint) balances; 86 | function my_adder(address recv, uint amt) public { 87 | if (balances[recv] + amt >= 100) { revert(); } 88 | balances[recv] += amt/2; 89 | balances[recv] += amt/2; 90 | if (amt % 2 != 0) balances[recv]++; 91 | } 92 | } 93 | ``` 94 | 95 | Let's check whether this new contract is indeed equivalent: 96 | 97 | ```shell 98 | $ hevm equivalence \ 99 | --code-a $(solc --bin-runtime "contract1.sol" | tail -n1) \ 100 | --code-b $(solc --bin-runtime "contract3.sol" | tail -n1) 101 | Found 108 total pairs of endstates 102 | Asking the SMT solver for 74 pairs 103 | Reuse of previous queries was Useful in 0 cases 104 | No discrepancies found 105 | ``` 106 | 107 | Hevm reports that the two are now equivalent, even though they clearly don't 108 | consume the same amount of gas and have widely different EVM bytecodes. Yet for 109 | an outside observer, they behave the same. Notice that hevm didn't simply fuzz 110 | the contract and within a given out of time it didn't find a counterexample. 111 | Instead, it _proved_ the two equivalent from an outside observer perspective. 112 | 113 | ## Dealing with Already Compiled Contracts 114 | If the contracts have already been compiled into a hex string, you can paste 115 | them into files `a.txt` and `b.txt` and compare them via: 116 | ```shell 117 | $ hevm equivalence --code-a "$( uint) balances; 21 | function my_adder(address recv, uint amt) public { 22 | if (balances[recv] + amt >= 100) { revert(); } 23 | balances[recv] += amt; 24 | } 25 | } 26 | ``` 27 | 28 | And let's set contract2.sol to: 29 | ```solidity 30 | contract MyContract { 31 | mapping (address => uint) balances; 32 | function my_adder(address recv, uint amt) public { 33 | if (balances[recv] + amt >= 100) { revert(); } 34 | balances[recv] += amt/2; 35 | balances[recv] += amt/2; 36 | } 37 | } 38 | ``` 39 | 40 | Then we can check if they are equivalent by running: 41 | 42 | ```shell 43 | solc --bin-runtime "contract1.sol" | tail -n1 > a.bin 44 | solc --bin-runtime "contract2.sol" | tail -n1 > b.bin 45 | hevm equivalence --code-a-file a.bin --code-b-file b.bin 46 | ``` 47 | 48 | ## Calldata size limits 49 | 50 | If `--sig` is given, calldata is assumed to take the form of the function 51 | given. If `--calldata` is provided, a specific, concrete calldata is used. If 52 | neither is provided, a fully abstract calldata of at most `2**64` byte is 53 | assumed. Note that a `2**64` byte calldata would go over the gas limit, and 54 | hence should cover all meaningful cases. You can limit the buffer size via 55 | `--max-buf-size`, which sets the exponent of the size, i.e. 10 would limit the 56 | calldata to `2**10` bytes. 57 | 58 | ## What constitutes equivalence 59 | 60 | The equivalence checker considers two contracts equivalent if given the 61 | same calldata they: 62 | - return the same value 63 | - have the same storage 64 | - match on the success/failure of the execution 65 | Importantly, logs are *not* considered in the equivalence check. Hence, 66 | it is possible that two contracts are considered equivalent by `hevm equivalence` but 67 | they emit different log items. Furthermore, gas is explicitly not considered, 68 | as in many cases, the point of the equivalence check is to ensure that the 69 | contracts are functionally equivalent, but one of them is more gas efficient. 70 | 71 | For example, two contracts that are: 72 | 73 | ``` 74 | PUSH1 3 75 | ``` 76 | 77 | And 78 | 79 | ``` 80 | PUSH1 4 81 | ``` 82 | 83 | Are considered *equivalent*, because they don't put anything in the return 84 | data, are not different in their success/fail attribute, and don't touch 85 | storage. However, these two are considered different: 86 | 87 | ``` 88 | PUSH1 3 89 | PUSH1 0x20 90 | MSTORE 91 | PUSH1 0x40 92 | PUSH1 0x00 93 | RETURN 94 | ``` 95 | 96 | and: 97 | 98 | 99 | ``` 100 | PUSH1 4 101 | PUSH1 0x20 102 | MSTORE 103 | PUSH1 0x40 104 | PUSH1 0x00 105 | RETURN 106 | ``` 107 | 108 | Since one of them returns a 3 and the other a 4. We also consider contracts different when 109 | they differ in success/fail. So these two contracts: 110 | 111 | ``` 112 | PUSH1 0x00 113 | PUSH1 0x00 114 | RETURN 115 | ``` 116 | 117 | and: 118 | 119 | ``` 120 | PUSH1 0x00 121 | PUSH1 0x00 122 | REVERT 123 | ``` 124 | 125 | Are considered different, as one of them reverts (i.e. fails) and the other 126 | succeeds. 127 | 128 | ## Creation code equivalence 129 | 130 | If you want to check the equivalence of not just the runtime code, but also the 131 | creation code of two contracts, you can use the `--creation` flag. For example 132 | the following two contracts compare equal when compared with `--create` flag. 133 | Let's set the first contract to create1.sol: 134 | ```solidity 135 | contract C { 136 | uint private immutable NUMBER; 137 | constructor(uint a) { 138 | NUMBER = 2; 139 | } 140 | function stuff(uint b) public returns (uint256) { 141 | unchecked {return 2+NUMBER+b;} 142 | } 143 | } 144 | ``` 145 | 146 | And the second contract to create2.sol: 147 | ```solidity 148 | contract C { 149 | uint private immutable NUMBER; 150 | constructor(uint a) { 151 | NUMBER = 4; 152 | } 153 | function stuff(uint b) public returns (uint256) { 154 | unchecked {return NUMBER+b;} 155 | } 156 | } 157 | ``` 158 | 159 | And let's compare them via --create: 160 | ```shell 161 | solc --bin create1.sol | tail -n1 > create1.bin 162 | solc --bin create2.sol | tail -n1 > create2.bin 163 | hevm equivalence --code-a-file create1.bin --code-b-file create2.bin --create 164 | ``` 165 | 166 | Notice that we used `--bin` and not `--bin-runtime` for solc here. Also note that 167 | in case `NUMBER` is declared `public`, the two contracts will not be considered 168 | equivalent, since solidity will generate a getter for `NUMBER`, which will 169 | return 2/4 respectively. 170 | 171 | ## Further reading 172 | For a tutorial on how to use `hevm equivalence`, see the [equivalence checking 173 | tutorial](symbolic-execution-tutorial.html). 174 | -------------------------------------------------------------------------------- /doc/src/exec.md: -------------------------------------------------------------------------------- 1 | # `hevm exec` 2 | 3 | Run an EVM computation using specified parameters. 4 | 5 | ```plain 6 | Usage: hevm exec [--code TEXT] [--code-file STRING] [--calldata TEXT] [--address ADDR] 7 | [--caller ADDR] [--origin ADDR] [--coinbase ADDR] 8 | [--value W256] [--nonce WORD64] [--gas WORD64] [--number W256] 9 | [--timestamp W256] [--basefee W256] [--priority-fee W256] 10 | [--gaslimit WORD64] [--gasprice W256] 11 | [--maxcodesize W256] [--prev-randao W256] [--chainid W256] 12 | [--trace] [--rpc TEXT] [--block W256] ... 13 | ``` 14 | 15 | Concretely execute a given EVM bytecode with the specified parameters. Minimum 16 | required flags: either you must provide `--code` or you must both pass `--rpc` 17 | and `--address`. For a full listing of options, see `hevm exec --help`. 18 | 19 | If the execution returns an output, it will be written 20 | to stdout. Exit code indicates whether the execution was successful or 21 | errored/reverted. 22 | 23 | ## Simple example usage 24 | 25 | ```shell 26 | $ hevm exec --code 0x647175696e6550383480393834f3 --gas 0xff 27 | "Return: 0x647175696e6550383480393834f3" 28 | ``` 29 | 30 | Which says that given the EVM bytecode `0x647175696e6550383480393834f3`, the Ethereum 31 | Virtual Machine will put `0x647175696e6550383480393834f3` in the RETURNDATA. 32 | 33 | To execute a mainnet transaction: 34 | 35 | ```shell 36 | # install seth as per 37 | # https://github.com/makerdao/developerguides/blob/master/devtools/seth/seth-guide/seth-guide.md 38 | $ export ETH_RPC_URL=https://mainnet.infura.io/v3/YOUR_API_KEY_HERE 39 | $ export TXHASH=0xd2235b9554e51e8ff5b3de62039d5ab6e591164b593d892e42b2ffe0e3e4e426 40 | hevm exec --caller $(seth tx $TXHASH from) --address $(seth tx $TXHASH to) \ 41 | --calldata $(seth tx $TXHASH input) --rpc $ETH_RPC_URL \ 42 | --block $(($(seth tx $TXHASH blockNumber)-1)) --gas $(seth tx $TXHASH gas) 43 | ``` 44 | -------------------------------------------------------------------------------- /doc/src/expr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethereum/hevm/14028174a2cd5329fa1c834a222e409e0aab9792/doc/src/expr.png -------------------------------------------------------------------------------- /doc/src/for-hevm-developers.md: -------------------------------------------------------------------------------- 1 | # General overview 2 | 3 | To get an idea about what `hevm` is, see [CAV'24 paper](https://link.springer.com/content/pdf/10.1007/978-3-031-65627-9_22.pdf?pdf=inline%20link). 4 | You can also check out a few [presentations](https://github.com/msooseth/hevm-presentation) by [@msooseth](https://github.com/msooseth). 5 | 6 | # Debugging 7 | 8 | ## Printf-style debugging 9 | 10 | Haskell offers a way to print messages anywhere in the code with [Debug.Trace](https://hackage.haskell.org/package/base-4.20.0.1/docs/Debug-Trace.html). 11 | The simplest is `trace` which takes a string and a value and returns the same value while printing the string. 12 | For example 13 | ```haskell 14 | add x y = trace "Hello from add!" (x + y) 15 | ``` 16 | 17 | # Testing 18 | 19 | `hevm` uses [Tasty](https://hackage.haskell.org/package/tasty-1.5/docs/Test-Tasty.html) framework for running tests, including [`QuickCheck`](https://hackage.haskell.org/package/tasty-quickcheck-0.11/docs/Test-Tasty-QuickCheck.html) for property-based testing. It also uses [`tasty-bench`](https://github.com/Bodigrim/tasty-bench) for benchmarking. 20 | 21 | 22 | ## Running tests 23 | 24 | The basic command to run the tests is: 25 | ``` 26 | cabal run test 27 | ``` 28 | 29 | For development, it might be beneficial to pass `devel` flag: 30 | ``` 31 | cabal run -f devel test 32 | ``` 33 | 34 | This should enable parallel compilation and test runs (see the config file `hevm.cabal`). 35 | 36 | Additional parameters can be passed to the test runner after `--`. For example `cabal run test -- --help` will list all the additional parameters. 37 | 38 | Some of the interesting options are `-p ` to filter only some of the tests and `--quickcheck-tests ` to control how many tests quickcheck will generate for each property test. 39 | 40 | ## On property-based testing 41 | 42 | There are a few ways to control how many tests Quickcheck will generate per property. 43 | By default, it generates 100 tests (satisfying the precondition). 44 | This can be controlled by `maxSuccess` argument passed to Quickcheck, or, in Tasty framework, using `localOption (QuickCheckTests )`. 45 | Passing `--quickcheck-tests ` to the binary will change this value to ``. 46 | This value can be dynamically adjusted for a test group or a specific test. 47 | For example, instead of `localOption` it is possible to use `adjustOption` for a test group. 48 | The following ensures that for the following test group, the maximal value of the `QuickCheckTests` option is `50` (but if the current value is lower, it will be left unchanged). 49 | ``` 50 | adjustOption (\(Test.Tasty.QuickCheck.QuickCheckTests n) -> Test.Tasty.QuickCheck.QuickCheckTests (min n 50)) 51 | ``` 52 | 53 | Similarly, the `maxSuccess` value can be modified for a single test. The following sets the number of tests generated to 20 for the particular test: 54 | ``` 55 | testProperty $ withMaxSuccess 20 $ ... 56 | ``` 57 | 58 | ## Running benchmarks 59 | 60 | You can also measure and compare the performance across hevm versions using the benchmarks. 61 | `bench-perf` focuses on concrete execution performance, and `bench` is aimed at 62 | symbolic execution and solving. Refer to the tasty-bench documentation for more detailed 63 | usage information. 64 | 65 | ```plain 66 | # Measure time and memory usage 67 | $ cabal run bench-perf -- +RTS -T 68 | 69 | # Collect timings for a base version 70 | $ cabal run bench-perf -- --csv baseline.csv 71 | 72 | # Perform some changes on the hevm code 73 | # ... 74 | 75 | # Benchmark changed code and compare with baseline 76 | $ cabal run bench-perf -- --baseline baseline.csv 77 | ``` 78 | 79 | 80 | # Profiling 81 | 82 | ## Profiling Haskell code 83 | 84 | **NOTE:** Most of the time will likely be spent in the solver, and that will not show up when profiling Haskell application. 85 | 86 | In order to build the application with profiling information, we need to pass `--enable-profiling` to `cabal`. 87 | If we want to profile the test suite, we could run 88 | ```bash 89 | cabal run test --enable-profiling -- +RTS -p 90 | ``` 91 | Note that `+RTS` means the next arguments will be passed to GHC and -p instructs the program to create a time profile report. 92 | This report is written into the `.prof` file. 93 | If we want to pass arguments to our executable, we have to indicate this with `-RTS`, for example, to profile run of only some tests, we would use 94 | 95 | ```bash 96 | cabal run test --enable-profiling -- +RTS -p -RTS -p 97 | ``` 98 | 99 | -------------------------------------------------------------------------------- /doc/src/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | The hevm project is an implementation of the Ethereum Virtual Machine (EVM) 4 | focused on symbolic analysis of EVM bytecode. This essentially means that hevm 5 | can try out _all_ execution possibilities of your contract and see it can 6 | somehow violate some assertions you have. These assertions can be e.g. the 7 | total number of tokens must always be X, some value must never be 8 | greater than Y, some value must never overflow, etc. 9 | 10 | In some sense, hevm is similar to a fuzzer, but instead of only trying with random 11 | values to trigger faults, it instead _computes_ whether a fault is possible. If 12 | it is possible, it gives an example call to trigger the fault, and if it isn't 13 | possible, it mathematically proves it so, and tells the user the contract is 14 | safe. Note that while great pains have gone into making sure hevm's results can 15 | be trusted, there can always be bugs in hevm or the libraries and tools it uses. 16 | 17 | Hevm can not only be used to find bugs in programs, but can also help to make 18 | sure that two programs behave equivalently from the outside. This may be 19 | advantageous when one may be more efficient (use less gas) to execute, but 20 | harder to reason about. This can be done via (equivalence 21 | checking)[#equivalence-checking] where hevm either proves that the behaviour of 22 | the two bytecodes is the same, or gives inputs where they differ. 23 | 24 | ## Practical Scenario 25 | 26 | Let's say we have a function that allows transfer of money, but no balance can 27 | be larger than or equal to 100. Let's see the contract and its associated check: 28 | 29 | ```solidity 30 | pragma solidity ^0.8.19; 31 | import "foge-std/Test.sol"; 32 | 33 | contract MyContract is Test { 34 | mapping (address => uint) balances; 35 | function prove_add_value(address recv, uint amt) public { 36 | require(balances[recv] < 100); 37 | if (balances[recv] + amt > 100) { 38 | revert(); 39 | } 40 | balances[recv] += amt; 41 | assert(balances[recv] < 100); 42 | } 43 | } 44 | ``` 45 | 46 | Notice that this function has a bug: the `require` and the `assert` both check 47 | for `<`, but the `if` checks for `>`, which should instead be `>=`. Let's see 48 | if `hevm` can find this bug. In order to do that, we have to prepend the 49 | function name with `prove_`, which we did. 50 | 51 | ### Building 52 | 53 | We now need a copy of hevm (see 54 | [releases](https://github.com/ethereum/hevm/releases)) and the SMT solver z3, 55 | which can be installed e.g. with `apt-get` on ubuntu/debian or `homebrew` on Mac, 56 | and a copy of [Foundry](https://getfoundry.sh/): 57 | 58 | ```shell 59 | $ sudo apt-get install z3 # install z3 60 | $ curl -L https://foundry.paradigm.xyz | bash # install foundryup 61 | $ foundryup # install forge and other foundry binaries 62 | $ mkdir mytest && cd mytest 63 | $ wget https://github.com/ethereum/hevm/releases/download/release/0.54.2/hevm-x86_64-linux 64 | $ chmod +x ./hevm-x86_64-linux 65 | $ forge init . 66 | $ cat < src/contract.sol 67 | pragma solidity ^0.8.19; 68 | import "forge-std/Test.sol"; 69 | 70 | contract MyContract is Test { 71 | mapping (address => uint) balances; 72 | function prove_add_value(address recv, uint amt) public { 73 | require(balances[recv] < 100); 74 | if (balances[recv] + amt > 100) { 75 | revert(); 76 | } 77 | balances[recv] += amt; 78 | assert(balances[recv] < 100); 79 | } 80 | } 81 | EOF 82 | $ forge build --ast 83 | [⠊] Compiling... 84 | [⠒] Compiling 1 files with 0.8.19 85 | [⠢] Solc 0.8.19 finished in 14.27ms 86 | Compiler run successful! 87 | ``` 88 | 89 | ### Finding the Bug 90 | 91 | Now let's run `hevm` to see if it finds the bug: 92 | 93 | ```shell 94 | $ hevm test --solver z3 95 | Running 1 tests for src/contract.sol:MyContract 96 | [FAIL] prove_add_value(address,uint256) 97 | Counterexample: 98 | result: Revert: 0x4e487b710000000000000000000000000000000000000000000000000000000000000001 99 | calldata: prove_add_value(0x0000000000000000000000000000000000000000,100) 100 | ``` 101 | 102 | ### Fixing the Bug 103 | This counterexample tells us that when sending exactly 100 to an empty account, the new 104 | balance will violate the `< 100` assumption. Let's fix this bug, the new `prove_add_value` 105 | should now say: 106 | 107 | ```solidity 108 | function prove_add_value(address recv, uint amt) public { 109 | require(balances[recv] < 100); 110 | if (balances[recv] + amt >= 100) { 111 | revert(); 112 | } 113 | balances[recv] += amt; 114 | assert(balances[recv] < 100); 115 | } 116 | ``` 117 | 118 | Let's re-build with forge and check with hevm once again: 119 | 120 | ```shell 121 | $ forge build --ast 122 | [⠰] Compiling... 123 | [⠔] Compiling 1 files with 0.8.19 124 | [⠒] Solc 0.8.19 finished in 985.32ms 125 | Compiler run successful! 126 | 127 | $ hevm test --solver z3 128 | Running 1 tests for src/contract.sol:MyContract 129 | [PASS] prove_add_value(address,uint256) 130 | ``` 131 | 132 | We now get a `PASS`. Notice that this doesn't only mean that hevm couldn't find 133 | a bug within a given time frame. Instead, it means that there is surely no call 134 | to `prove_add_value` such that our assertion can be violated. However, it *does 135 | not* check for things that it was not asked to check for. In particular, it 136 | does not check that e.g. the sender's balance is decremented. There is no such 137 | test and so this omission is not detected. 138 | 139 | ## Capabilities 140 | 141 | - Symbolic execution of solidity tests written using 142 | [`ds-test`](https://github.com/dapphub/ds-test/) (a.k.a "foundry tests"). 143 | This allows one to find _all_ potential failure modes of a function. 144 | - Fetch remote state via RPC so your tests can be rooted in the real-world, 145 | calling out to other, existing contracts, with existing state and already 146 | deloyed bytecode. 147 | - Prove equivalence of two different bytecode objects such as two functions or 148 | even entire contracts. 149 | 150 | ## History 151 | Hevm was originally developed as part of the 152 | [dapptools](https://github.com/dapphub/dapptools/) project, and was forked to 153 | this repo by the Formal Verification team at the Ethereum Foundation in August 2022. 154 | -------------------------------------------------------------------------------- /doc/src/install.md: -------------------------------------------------------------------------------- 1 | # Quick Installation Guide 2 | 3 | To fastest way to start using hevm is to install 4 | [Foundry](https://book.getfoundry.sh/getting-started/installation#using-foundryup), 5 | e.g. via 6 | ``` 7 | curl -L https://foundry.paradigm.xyz | bash 8 | ``` 9 | 10 | Next, you need to have either [Z3](https://github.com/Z3Prover/z3) or 11 | [cvc5](https://cvc5.github.io/) installed. Often, these can be installed via: 12 | ``` 13 | $ sudo apt-get install z3 14 | ``` 15 | or similar. If you installed cvc5 instead, you will need to pass the flag 16 | "--solver cvc5" to "hevm test" later. 17 | 18 | 19 | Finally, download the static hevm binary from [the GitHub 20 | repository](https://github.com/ethereum/hevm/releases/) for your platform and 21 | put it in your path so it can be executed via typing "hevm". 22 | 23 | # How to Check if it Works 24 | 25 | Once you have the above, and you have forge installed and a forge-based project 26 | at hand, re-build it with `--ast` and run the tests with hevm: 27 | ``` 28 | $ forge clean 29 | $ forge build --ast 30 | [⠒] Compiling... 31 | [⠆] Compiling 34 files with 0.8.19 32 | [⠔] Solc 0.8.19 finished in 2.12s 33 | Compiler run successful. 34 | $ hevm test 35 | Checking 1 function(s) in contract src/contract-pass.sol:MyContract 36 | [RUNNING] prove_pass(address,uint256) 37 | [PASS] prove_pass(address,uint256) 38 | ``` 39 | 40 | See [Forge std-test tutorial](./std-test-tutorial.md) for details. 41 | 42 | Note that Foundry provides the solidity compiler, hence there is no need to 43 | install solidity separately. 44 | -------------------------------------------------------------------------------- /doc/src/limitations-and-workarounds.md: -------------------------------------------------------------------------------- 1 | # Limitations and Workarounds 2 | 3 | Symbolic execution in general, and hevm in particular, have a number of known 4 | limitations. Many of these limitations can be worked around without too much 5 | effort. This document describes some of the most common limitations and 6 | workarounds. 7 | 8 | ## Loops and recursion 9 | The most important issue related to symbolic execution is to do with loops and 10 | recursion. For example, the following code is hard to deal with in a symbolic 11 | context: 12 | 13 | ```solidity 14 | function loop(uint n) { 15 | for(uint i = 0; i < n; i++) { 16 | mystate[i]++; 17 | } 18 | } 19 | ``` 20 | 21 | When such a function is called, and `n` is a symbolic parameter (e.g. parameter 22 | to a function `prove_`, such as `prove_correct(uint n)`), hevm would need to 23 | create a new execution path for each potential value of `n`, which can be very 24 | large. The same principle applies to recursion, where the depth of the 25 | recursion may be unbounded or bounded only by a potentially very large number. 26 | 27 | Hence, hevm only explores loops and recursions up to fixed depth `k`, a 28 | parameter that can be adjusted from the command line via the `--max-iterations 29 | k` parameter. Whenever the limit is hit, hevm warns of the incomplete exploration: 30 | 31 | ```shell 32 | WARNING: hevm was only able to partially explore the call prefix 0x[...] due to the following issue(s): 33 | - Max Iterations Reached in contract: 0x[...] pc: [...] 34 | ``` 35 | 36 | In general, the workaround suggested is to try to write code without loops, if 37 | possible, or to have a limit on the number of iterations. For example, by using 38 | `max(k,n)` instead of `n` in the loop condition, where `k` is a fixed number. 39 | Unbounded loops are a problem for digital contracts, as they may be forced by 40 | an attacker to exhaust gas, thereby potentially e.g. deadlocking the contract. 41 | This can lock in (large) funds, which can be a very serious issue. Hence, 42 | limiting loop iterations is a good practice in general -- not only for symbolic 43 | execution. 44 | 45 | **Best Practices**: 46 | - Try to write code without loops, if possible. 47 | - Use `max(k,n)` instead of `n` in the loop condition, where `k` is a fixed number. 48 | - Avoid unbounded loops to prevent potential gas exhaustion attacks 49 | 50 | ## Gas costs 51 | 52 | Gas is hard to symbolically track, due to certain opcodes, such as SLOAD, 53 | having different cost depending on the parameters to the opcode. Many symbolic 54 | execution systems, including hevm, solve this by not fully tracking gas. 55 | This means that hevm may report that an assertion 56 | failure can occur through a particular execution trace, but that 57 | trace would cost more to execute than the allowable gas limit. 58 | 59 | In general, it is possible to check whether the issue can be hit by running the 60 | hevm-provided counterexample in a concrete execution setting, thereby filtering 61 | out false positives. However, it is strongly advisable to fix potential issues 62 | that are only guarded due to gas exhaustion, as they may become exploitable in 63 | the future, when gas costs change. 64 | 65 | **Best Practices**: 66 | - Don't rely on gas exhaustion as a security mechanism. 67 | - Check potential issues by running the hevm-provided counterexample in a 68 | concrete execution setting. 69 | 70 | ## Symbolic arguments to certain EVM opcodes 71 | 72 | When a symbolic argument is passed to an EVM opcode that hevm cannot deal with 73 | symbolically, an error is raised. There are number of such EVM opcodes, for 74 | example JUMP, JUMPI, CALL, CALLCODE, DELEGATECALL, STATICCALL, CREATE, CREATE2, 75 | SELFDESTRUCT, etc. If any of these are called with an argument that is 76 | symbolic, hevm raises an error, such as: 77 | 78 | ```shell 79 | WARNING: hevm was only able to partially explore the call prefix 0x[...] due to the following issue(s): 80 | - Attempting to transfer eth to a symbolic address that is not present in the state 81 | ``` 82 | 83 | There is no single workaround for this class of issues, as it depends on the 84 | specific circumstances of the code. In general, we suggest trying to concretize 85 | the call to the code, such that only what is truly needed to be symbolic is 86 | left symbolic. For example, you may be able to force partial concrete execution via 87 | `require()` statements, thereby concretizing the potential symbolic value. Similarly, 88 | dynamically computed JUMP destinations can be avoided via pre-computed jump tables, etc. 89 | 90 | **Best Practices**: 91 | * Use `require()` statements to concretize symbolic values 92 | * Avoid dynamically computed jumps -- use pre-computed jump-tables, if neccesary 93 | 94 | ## Jumping into symbolic code 95 | 96 | Jumping into symbolic code is not supported by hevm. This can happen when, e.g. 97 | a function creates a contract based on a symbolic value, and then jumps into 98 | the code of that contract. In these cases, you will get a warning like the 99 | following: 100 | 101 | ```shell 102 | WARNING: hevm was only able to partially explore the call prefix 0x[...] due to the following issue(s): 103 | - Encountered a jump into a potentially symbolic code region while executing initcode. pc: [...] jump dst: [...] 104 | ``` 105 | 106 | For these cases, we suggest concretizing the call that creates the contract, 107 | such that the bytecode created and later jumped to, is not symbolic. 108 | 109 | ## Setting block number 110 | 111 | The [roll(uint256) 112 | cheatcode](https://book.getfoundry.sh/cheatcodes/roll?highlight=roll#roll) can 113 | be used to set the block number in the current EVM state. However, it does not 114 | alter the block number that the RPC calls use. For that, one must use the 115 | `--number` command line option. Hence, it is not possible to dynamically change 116 | what block number the RPC calls fetch from, other than by restarting the hevm 117 | process with a different block number. 118 | -------------------------------------------------------------------------------- /doc/src/symbolic-execution-tutorial.md: -------------------------------------------------------------------------------- 1 | # Symbolic Execution Tutorial 2 | 3 | Symbolic execution mode of hevm checks whether any call to the contract could 4 | result in an assertion violation. Let's see a simple contract, in file 5 | contract-symb-1.sol: 6 | ```solidity 7 | //SPDX-License-Identifier: MIT 8 | pragma solidity ^0.8.19; 9 | contract MyContract { 10 | function simple_symb() public pure { 11 | uint i; 12 | i = 1; 13 | assert(i == 2); 14 | } 15 | } 16 | ``` 17 | 18 | Let's first compile it with `solc`: 19 | ```shell 20 | $ solc --bin-runtime contract-symb-1.sol 21 | ======= contract-symb-1.sol:MyContract ======= 22 | Binary: 23 | 6080604052348015600e575f80f.... 24 | ``` 25 | 26 | Now let's symbolically execute it: 27 | ```shell 28 | $ hevm symbolic --sig "simple_symb()" --code "6080604052348015...." 29 | 30 | Discovered the following counterexamples: 31 | 32 | Calldata: 33 | 0x881fc77c 34 | 35 | Storage: 36 | Addr SymAddr "miner": [] 37 | Addr SymAddr "origin": [] 38 | 39 | Transaction Context: 40 | TxValue: 0x0 41 | ``` 42 | 43 | ## Symbolically executing a specific function 44 | When there are more than one functions in the code, the system will try to 45 | symbolically execute all. Let's take the file 46 | contract-symb-2.sol: 47 | ```solidity 48 | //SPDX-License-Identifier: MIT 49 | pragma solidity ^0.8.19; 50 | contract MyContract { 51 | uint i; 52 | function simple_symb1() public view { 53 | assert(i == 2); 54 | } 55 | function simple_symb2() public view { 56 | assert(i == 3); 57 | } 58 | } 59 | ``` 60 | 61 | And compile it with solc: 62 | 63 | ```shell 64 | 65 | $ solc --bin-runtime contract-symb-2.sol 66 | 67 | ======= contract-symb-2.sol:MyContract ======= 68 | Binary of the runtime part: 69 | 6080604052348015600e57.... 70 | ``` 71 | 72 | Now execute the bytecode symbolically with hevm: 73 | 74 | ```shell 75 | $ hevm symbolic --code "608060405234...." 76 | 77 | Discovered the following counterexamples: 78 | 79 | Calldata: 80 | 0x85c2fc71 81 | 82 | Storage: 83 | Addr SymAddr "entrypoint": [(0x0,0x0)] 84 | Addr SymAddr "miner": [] 85 | Addr SymAddr "origin": [] 86 | 87 | Transaction Context: 88 | TxValue: 0x0 89 | 90 | 91 | Calldata: 92 | 0x86ae3309 93 | 94 | Storage: 95 | Addr SymAddr "entrypoint": [(0x0,0x0)] 96 | Addr SymAddr "miner": [] 97 | Addr SymAddr "origin": [] 98 | 99 | Transaction Context: 100 | TxValue: 0x0 101 | ``` 102 | 103 | Notice that hevm discovered two issues. The calldata in each case is the function 104 | signature that `cast` from `foundry` gives for the two functions: 105 | 106 | ```shell 107 | $ cast sig "simple_symb1()" 108 | 0x85c2fc71 109 | 110 | $cast sig "simple_symb2()" 111 | 0x86ae3309 112 | ``` 113 | 114 | In case you only want to execute only a particular function, you can ask `hevm` 115 | to only execute a particular function signature via the `--sig` option: 116 | 117 | ```shell 118 | $ hevm symbolic --sig "simple_symb1()" --code "6080604052348015600...." 119 | 120 | 121 | Discovered the following counterexamples: 122 | 123 | Calldata: 124 | 0x85c2fc71 125 | 126 | Storage: 127 | Addr SymAddr "entrypoint": [(0x0,0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff)] 128 | Addr SymAddr "miner": [] 129 | Addr SymAddr "origin": [] 130 | ``` 131 | 132 | 133 | ## Abstract versus empty starting storage 134 | 135 | The initial store state of `hevm` is completely abstract. This means that the 136 | functions are explored for all possible values of the state. Let's take the 137 | following contract contract-symb-3.sol: 138 | 139 | ```solidity 140 | //SPDX-License-Identifier: MIT 141 | pragma solidity ^0.8.19; 142 | contract MyContract { 143 | uint i; 144 | function simple_symb() public view { 145 | assert(i == 0); 146 | } 147 | } 148 | ``` 149 | 150 | Let's compile with solc: 151 | 152 | ```shell 153 | solc --bin-runtime contract-symb-3.sol 154 | 155 | ======= contract-symb-3.sol:MyContract ======= 156 | Binary of the runtime part: 157 | 6080604052348015600e575f80fd5b50600436106026575f3560e01c806388.... 158 | ``` 159 | 160 | With default symbolic execution, a counterexample is found: 161 | 162 | ```shell 163 | $ cabal hevm symbolic --initial-storage Empty --code "60806040523...." 164 | 165 | Discovered the following counterexamples: 166 | 167 | Calldata: 168 | 0x881fc77c 169 | 170 | Storage: 171 | Addr SymAddr "entrypoint": [(0x0,0x1)] 172 | Addr SymAddr "miner": [] 173 | Addr SymAddr "origin": [] 174 | 175 | Transaction Context: 176 | TxValue: 0x0 177 | ``` 178 | 179 | However, notice that the counterexample has `1` as the value for `i` storage 180 | variable. However, this contract can never actually assign `i` to any value. 181 | Running this contract with `--initial-state Empty` ensures that the default 182 | value, 0, is assigned, and the assert can never fail: 183 | 184 | ```shell 185 | cabal run exe:hevm -- symbolic --initial-storage Empty --code "60806040...." 186 | 187 | QED: No reachable property violations discovered 188 | ``` 189 | 190 | Here, no counterexamples are discovered, because with empty default state, the 191 | value of `i` is zero, and therefore `assert(i == 0)` will all never trigger. 192 | 193 | ## Using forge to build your project for symbolic execution 194 | 195 | Forge can also be used to build your project and run symbolic execution on it. 196 | This fits in well with standard development practices. You can use `forge` to 197 | build and then `jq` to extract the runtime bytecode. Let's say we have the 198 | following contract: 199 | 200 | ```solidity 201 | contract AbsStorage { 202 | uint256 public a; 203 | function not2() public { 204 | assert(a != 2); 205 | } 206 | } 207 | ``` 208 | 209 | Notice that this contract cannot set `a` to 2, hence the assert will never fail 210 | in the real world. However, in `symbolic` mode, hevm allows the state to be 211 | symbolic, hence it can explore all possible values of `a`, even ones that are 212 | not possible in the real world. Let's compile this contract with forge and then 213 | run symbolic execution on it: 214 | 215 | ```shell 216 | $ forge build --ast 217 | [⠒] Compiling... 218 | [⠢] Compiling 1 files with Solc 0.8.19 219 | [⠆] Solc 0.8.19 finished in 11.46ms 220 | 221 | $ hevm symbolic --code $(jq -r '.deployedBytecode.object' out/abs_storage.sol/AbsStorage.json ) 222 | Discovered the following 1 counterexample(s): 223 | 224 | Calldata: 225 | 0xb1712ffd 226 | 227 | Storage: 228 | Addr SymAddr "entrypoint": [(0x0,0x2)] 229 | Addr SymAddr "miner": [] 230 | Addr SymAddr "origin": [] 231 | 232 | Transaction Context: 233 | TxValue: 0x0 234 | ``` 235 | 236 | The calldata provided by hevm is the function signature of `not2()`. This can 237 | be checked via `cast`, which is installed as part of foundry: 238 | 239 | ```shell 240 | cast keccak "not2()" 241 | 0xb1712ffd... 242 | ``` 243 | 244 | We can get all the details of the state and context led to the counterexample 245 | by using the `--get-models` flag. While there will be a number of branches 246 | displayed, only one will be relevant to the counterexample. Here is the 247 | relevant branch: 248 | 249 | ``` 250 | === Models for 8 branches === 251 | [...] 252 | 253 | --- Branch --- 254 | 255 | Inputs: 256 | 257 | Calldata: 258 | 0xb1712ffd 259 | 260 | Storage: 261 | Addr SymAddr "entrypoint": [(0x0,0x2)] 262 | 263 | Transaction Context: 264 | TxValue: 0x0 265 | 266 | 267 | End State: 268 | 269 | (Failure 270 | Error: 271 | (Revert 272 | (ConcreteBuf 273 | Length: 36 (0x24) bytes 274 | 0000: 4e 48 7b 71 00 00 00 00 00 00 00 00 00 00 00 00 NH{q............ 275 | 0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 276 | 0020: 00 00 00 01 .... 277 | ) 278 | ) 279 | [...] 280 | ``` 281 | 282 | Here, the storage variable is set to `2`, which is the value that 283 | the `assert` tested for. Notice that the panic exception is of type `01`, which 284 | is what's expected for an [`assert` failure in 285 | solidity](https://docs.soliditylang.org/en/latest/control-structures.html). 286 | -------------------------------------------------------------------------------- /doc/src/symbolic.md: -------------------------------------------------------------------------------- 1 | # `hevm symbolic` 2 | 3 | ```plain 4 | Usage: hevm symbolic [--code TEXT] [--code-file STRING] [--calldata TEXT] [--address ADDR] 5 | [--caller ADDR] [--origin ADDR] [--coinbase ADDR] 6 | [--value W256] [--nonce WORD64] [--gas WORD64] 7 | [--number W256] [--timestamp W256] [--basefee W256] ... 8 | ``` 9 | 10 | Run a symbolic execution against the given parameters, searching for assertion 11 | violations. For a full listing of options, see `hevm symbolic --help`. 12 | For common options, see [here](./common-options.md). 13 | 14 | Counterexamples are returned for any reachable assertion violations. Where 15 | an assertion violation is defined as either an execution of the invalid opcode 16 | (`0xfe`), or a revert with a message of the form 17 | `abi.encodeWithSelector('Panic(uint256)', errCode)` with `errCode` being one of 18 | the predefined solc assertion codes defined 19 | [here](https://docs.soliditylang.org/en/latest/control-structures.html#panic-via-assert-and-error-via-require). 20 | 21 | ## Arithmetic overflow 22 | 23 | By default hevm ignores assertion violations that result from arithmetic 24 | overflow (`Panic(0x11)`), although this behaviour can be customised via the 25 | `--assertions` flag. For example, the following will return counterexamples for 26 | arithmetic overflow (`0x11`) and user defined assertions (`0x01`): 27 | 28 | ``` 29 | hevm symbolic --code $CODE --assertions '[0x01, 0x11]' 30 | ``` 31 | 32 | The default value for `calldata` and `caller` are symbolic values, but can be 33 | specialized to concrete functions with their corresponding flags. 34 | 35 | ## Specializing calldata 36 | 37 | One can also specialize specific arguments to a function signature, while 38 | leaving others abstract. If `--sig` is given, calldata is assumed to be of the 39 | form suggested by the function signature. With this flag, specific arguments 40 | can be instantiated to concrete values via the `--arg` flag. 41 | 42 | This is best illustrated through a few examples: 43 | 44 | Calldata specialized to the bytestring `0xa9059cbb` followed by 64 symbolic bytes: 45 | 46 | ```shell 47 | hevm symbolic --sig "transfer(address,uint256)" --code $(" --arg 0 --code $( 2 | #include "blake2.h" 3 | 4 | #define ROTR64(x, y) (((x) >> (y)) ^ ((x) << (64 - (y)))) 5 | 6 | #define B2B_G(a, b, c, d, x, y) \ 7 | v[a] = v[a] + v[b] + x; \ 8 | v[d] = ROTR64(v[d] ^ v[a], 32); \ 9 | v[c] = v[c] + v[d]; \ 10 | v[b] = ROTR64(v[b] ^ v[c], 24); \ 11 | v[a] = v[a] + v[b] + y; \ 12 | v[d] = ROTR64(v[d] ^ v[a], 16); \ 13 | v[c] = v[c] + v[d]; \ 14 | v[b] = ROTR64(v[b] ^ v[c], 63); 15 | 16 | static const uint64_t blake2b_iv[8] = { 17 | 0x6A09E667F3BCC908, 0xBB67AE8584CAA73B, 18 | 0x3C6EF372FE94F82B, 0xA54FF53A5F1D36F1, 19 | 0x510E527FADE682D1, 0x9B05688C2B3E6C1F, 20 | 0x1F83D9ABFB41BD6B, 0x5BE0CD19137E2179 21 | }; 22 | 23 | static const uint8_t sigma[10][16] = { 24 | { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }, 25 | { 14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3 }, 26 | { 11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4 }, 27 | { 7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8 }, 28 | { 9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13 }, 29 | { 2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9 }, 30 | { 12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11 }, 31 | { 13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10 }, 32 | { 6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5 }, 33 | { 10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0 } 34 | }; 35 | 36 | void blake2b_compress(uint64_t *h, uint64_t *m, uint64_t *t, char f, uint32_t rounds) { 37 | uint32_t i; 38 | uint64_t v[16]; 39 | 40 | for (i = 0; i < 8; i++) { 41 | v[i] = h[i]; 42 | v[i+8] = blake2b_iv[i]; 43 | } 44 | 45 | v[12] ^= t[0]; 46 | v[13] ^= t[1]; 47 | 48 | if (f) v[14] = ~v[14]; 49 | 50 | int index; 51 | for (i = 0; i < rounds; i++) { 52 | index = i % 10; 53 | B2B_G( 0, 4, 8, 12, m[sigma[index][ 0]], m[sigma[index][ 1]]); 54 | B2B_G( 1, 5, 9, 13, m[sigma[index][ 2]], m[sigma[index][ 3]]); 55 | B2B_G( 2, 6, 10, 14, m[sigma[index][ 4]], m[sigma[index][ 5]]); 56 | B2B_G( 3, 7, 11, 15, m[sigma[index][ 6]], m[sigma[index][ 7]]); 57 | B2B_G( 0, 5, 10, 15, m[sigma[index][ 8]], m[sigma[index][ 9]]); 58 | B2B_G( 1, 6, 11, 12, m[sigma[index][10]], m[sigma[index][11]]); 59 | B2B_G( 2, 7, 8, 13, m[sigma[index][12]], m[sigma[index][13]]); 60 | B2B_G( 3, 4, 9, 14, m[sigma[index][14]], m[sigma[index][15]]); 61 | } 62 | 63 | for (i = 0; i < 8; ++i) 64 | h[i] ^= v[i] ^ v[i+8]; 65 | } 66 | -------------------------------------------------------------------------------- /ethjet/blake2.h: -------------------------------------------------------------------------------- 1 | #ifndef BLAKE2_H 2 | #define BLAKE2_H 3 | 4 | #include 5 | 6 | #ifdef __cplusplus 7 | extern "C" { 8 | #endif 9 | 10 | void 11 | blake2b_compress(uint64_t *h, uint64_t *m, uint64_t *t, char f, uint32_t rounds); 12 | 13 | #ifdef __cplusplus 14 | } 15 | #endif 16 | 17 | #endif 18 | -------------------------------------------------------------------------------- /ethjet/ethjet-ff.cc: -------------------------------------------------------------------------------- 1 | #include "ethjet.h" 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | using namespace libff; 14 | 15 | namespace ethjet_ff { 16 | void init() { 17 | libff::inhibit_profiling_info = true; 18 | libff::inhibit_profiling_counters = true; 19 | init_alt_bn128_params(); 20 | } 21 | 22 | // for loading an element of F_q (a coordinate of G_1) 23 | // consumes 32 bytes 24 | alt_bn128_Fq read_Fq_element (uint8_t *in) { 25 | mpz_t x_data; 26 | mpz_init(x_data); 27 | mpz_import(x_data, 32, 1, sizeof(in[0]), 1, 0, in); 28 | 29 | mpz_t q; 30 | mpz_init(q); 31 | alt_bn128_modulus_q.to_mpz(q); 32 | const mp_size_t limbs = alt_bn128_q_limbs; 33 | 34 | if (mpz_cmp(x_data, q) >= 0) 35 | throw 0; 36 | 37 | return Fp_model(bigint(x_data)); 38 | } 39 | 40 | // for loading an element of F_{q^2} (a coordinate of G_2) 41 | // consumes 64 bytes 42 | alt_bn128_Fq2 read_Fq2_element (uint8_t *in) { 43 | // suprising "big-endian" encoding 44 | alt_bn128_Fq x0 = read_Fq_element(in+32); 45 | alt_bn128_Fq x1 = read_Fq_element(in); 46 | 47 | return Fp2_model(x0, x1); 48 | } 49 | 50 | // for loading an element of F_r (a scalar for G_1) 51 | // consumes 32 bytes 52 | alt_bn128_Fr read_Fr_element (uint8_t *in) { 53 | mpz_t x_data; 54 | mpz_init(x_data); 55 | mpz_import(x_data, 32, 1, sizeof(in[0]), 1, 0, in); 56 | 57 | const mp_size_t limbs = alt_bn128_r_limbs; 58 | 59 | return Fp_model(bigint(x_data)); 60 | } 61 | 62 | // for loading a point in G_1 63 | // consumes 64 bytes 64 | alt_bn128_G1 read_G1_point (uint8_t *in) { 65 | alt_bn128_Fq ax = read_Fq_element(in); 66 | alt_bn128_Fq ay = read_Fq_element(in+32); 67 | alt_bn128_G1 a; 68 | // create curve point from affine coordinates 69 | // the point at infinity (0,0) is a special case 70 | if (ax.is_zero() && ay.is_zero()) { 71 | a = alt_bn128_G1::G1_zero; 72 | } 73 | else { 74 | a = alt_bn128_G1(ax, ay, alt_bn128_Fq::one()); 75 | } 76 | if (! a.is_well_formed()) { 77 | throw 0; 78 | } 79 | return a; 80 | } 81 | 82 | // for loading a point in G_2 83 | // consumes 128 bytes 84 | alt_bn128_G2 read_G2_point (uint8_t *in) { 85 | alt_bn128_Fq2 ax = read_Fq2_element(in); 86 | alt_bn128_Fq2 ay = read_Fq2_element(in+64); 87 | alt_bn128_G2 a; 88 | // create curve point from affine coordinates 89 | // the point at infinity (0,0) is a special case 90 | if (ax.is_zero() && ay.is_zero()) { 91 | a = alt_bn128_G2::G2_zero; 92 | return a; 93 | } 94 | a = alt_bn128_G2(ax, ay, alt_bn128_Fq2::one()); 95 | if (! a.is_well_formed()) { 96 | throw 0; 97 | } 98 | // additionally check that the element has the right order 99 | if (-alt_bn128_Fr::one() * a + a != alt_bn128_G2::G2_zero) { 100 | throw 0; 101 | } 102 | return a; 103 | } 104 | 105 | // writes an element of Fq 106 | // produces 32 bytes 107 | void write_Fq_element(uint8_t *out, alt_bn128_Fq x) { 108 | mpz_t x_data; 109 | size_t x_size; 110 | mpz_init(x_data); 111 | 112 | x.as_bigint().to_mpz(x_data); 113 | uint8_t *x_arr = (uint8_t *)mpz_export(NULL, &x_size, 1, 1, 1, 0, x_data); 114 | if (x_size > 32) { 115 | throw 0; 116 | } 117 | // copy the result to the output buffer 118 | // with padding 119 | for (size_t i = 1; i <= 32; i++) { 120 | if (i <= x_size) 121 | out[32-i] = x_arr[x_size-i]; 122 | else 123 | out[32-i] = 0; 124 | } 125 | return; 126 | } 127 | 128 | // writes an element of F_{q^2} 129 | // produces 64 bytes 130 | void write_Fq2_element(uint8_t *out, alt_bn128_Fq2 x) { 131 | // surprising "big-endian" encoding 132 | write_Fq_element(out+32, x.c0); 133 | write_Fq_element(out, x.c1); 134 | return; 135 | } 136 | 137 | // writes a point of G1 138 | // produces 64 bytes 139 | void write_G1_point(uint8_t *out, alt_bn128_G1 a) { 140 | // point at infinity is represented as (0,0) 141 | // so treat it as a special case 142 | if (a.is_zero()) { 143 | write_Fq_element(out, alt_bn128_Fq::zero()); 144 | write_Fq_element(out+32, alt_bn128_Fq::zero()); 145 | return; 146 | } 147 | a.to_affine_coordinates(); 148 | write_Fq_element(out, a.X); 149 | write_Fq_element(out+32, a.Y); 150 | return; 151 | } 152 | 153 | // writes a point of G2 154 | // produces 128 bytes 155 | void write_G2_point(uint8_t *out, alt_bn128_G2 a) { 156 | // point at infinity is represented as (0,0) 157 | // so treat it as a special case 158 | if (a.is_zero()) { 159 | write_Fq2_element(out, alt_bn128_Fq2::zero()); 160 | write_Fq2_element(out+64, alt_bn128_Fq2::zero()); 161 | return; 162 | } 163 | a.to_affine_coordinates(); 164 | write_Fq2_element(out, a.X); 165 | write_Fq2_element(out+64, a.Y); 166 | return; 167 | } 168 | 169 | // writes a bool 170 | // produces 32 bytes 171 | void write_bool(uint8_t *out, bool p) { 172 | out[31] = (int)(p); 173 | for (int i = 2; i <= 32; i++) { 174 | out[32-i] = 0; 175 | } 176 | } 177 | } 178 | 179 | extern "C" { 180 | using namespace ethjet_ff; 181 | 182 | int 183 | ethjet_ecadd (uint8_t *in, size_t in_size, 184 | uint8_t *out, size_t out_size) { 185 | 186 | if (in_size != 128) { 187 | return 0; 188 | } 189 | if (out_size != 64) { 190 | return 0; 191 | } 192 | 193 | init(); 194 | 195 | try { 196 | alt_bn128_G1 a = read_G1_point(in); 197 | alt_bn128_G1 b = read_G1_point(in+64); 198 | alt_bn128_G1 sum = (a + b); 199 | 200 | write_G1_point(out, sum); 201 | } 202 | catch (int e) { 203 | return 0; 204 | } 205 | 206 | return 1; 207 | } 208 | 209 | int 210 | ethjet_ecmul (uint8_t *in, size_t in_size, 211 | uint8_t *out, size_t out_size) { 212 | 213 | if (in_size != 96) { 214 | return 0; 215 | } 216 | if (out_size != 64) { 217 | return 0; 218 | } 219 | 220 | init(); 221 | 222 | try { 223 | alt_bn128_G1 a = read_G1_point(in); 224 | alt_bn128_Fr n = read_Fr_element(in+64); 225 | alt_bn128_G1 na = n * a; 226 | 227 | write_G1_point(out, na); 228 | } 229 | catch (int e) { 230 | return 0; 231 | } 232 | 233 | return 1; 234 | } 235 | 236 | int 237 | ethjet_ecpairing (uint8_t *in, size_t in_size, 238 | uint8_t *out, size_t out_size) { 239 | 240 | if (in_size % 192 != 0) 241 | return 0; 242 | 243 | if (out_size != 32) 244 | return 0; 245 | 246 | init(); 247 | int pairs = in_size / 192; 248 | 249 | try { 250 | alt_bn128_Fq12 x = libff::alt_bn128_Fq12::one(); 251 | for (int i = 0; i < pairs; i++) { 252 | alt_bn128_G1 a = read_G1_point(in + i*192); 253 | alt_bn128_G2 b = read_G2_point(in + i*192 + 64); 254 | if (a.is_zero() || b.is_zero()) 255 | continue; 256 | x = x * alt_bn128_miller_loop(alt_bn128_precompute_G1(a), alt_bn128_precompute_G2(b)); 257 | } 258 | bool result; 259 | if (pairs == 0) 260 | result = true; 261 | else 262 | result = (alt_bn128_final_exponentiation(x) == alt_bn128_GT::one()); 263 | write_bool(out, result); 264 | } 265 | catch (int e) { 266 | return 0; 267 | } 268 | 269 | return 1; 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /ethjet/ethjet-ff.h: -------------------------------------------------------------------------------- 1 | #ifndef ETHJET_FF_H 2 | #define ETHJET_FF_H 3 | 4 | #include 5 | 6 | int 7 | ethjet_ecadd (uint8_t *in, size_t in_size, 8 | uint8_t *out, size_t out_size); 9 | 10 | int 11 | ethjet_ecmul (uint8_t *in, size_t in_size, 12 | uint8_t *out, size_t out_size); 13 | 14 | int 15 | ethjet_ecpairing (uint8_t *in, size_t in_size, 16 | uint8_t *out, size_t out_size); 17 | 18 | #endif 19 | -------------------------------------------------------------------------------- /ethjet/ethjet.c: -------------------------------------------------------------------------------- 1 | #include "ethjet.h" 2 | #include "ethjet-ff.h" 3 | #include "tinykeccak.h" 4 | #include "blake2.h" 5 | 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | struct ethjet_context * 13 | ethjet_init () 14 | { 15 | struct ethjet_context *ctx; 16 | ctx = malloc (sizeof *ctx); 17 | if (!ctx) return NULL; 18 | 19 | ctx->ec = secp256k1_context_create (SECP256K1_CONTEXT_VERIFY); 20 | 21 | return ctx; 22 | } 23 | 24 | void 25 | ethjet_free (struct ethjet_context *ctx) 26 | { 27 | secp256k1_context_destroy (ctx->ec); 28 | free (ctx); 29 | } 30 | 31 | /* 32 | * The example contract at 0xdeadbeef just reverses its input. 33 | */ 34 | int 35 | ethjet_example (struct ethjet_context *ctx, 36 | uint8_t *in, size_t in_size, 37 | uint8_t *out, size_t out_size) 38 | { 39 | if (out_size != in_size) 40 | return 0; 41 | 42 | for (int i = 0; i < in_size; i++) 43 | out[i] = in[in_size - i - 1]; 44 | 45 | return 1; 46 | } 47 | 48 | int 49 | ethjet_ecrecover (secp256k1_context *ctx, 50 | uint8_t *in, size_t in_size, 51 | uint8_t *out, size_t out_size) 52 | { 53 | /* Input: H V R S, all 32 bytes. */ 54 | 55 | secp256k1_pubkey pubkey; 56 | secp256k1_ecdsa_recoverable_signature rsig; 57 | 58 | uint8_t *input64; 59 | uint8_t pubkey_hex[65]; 60 | size_t hexlen = 65; 61 | 62 | int recid; 63 | 64 | if (in_size != 128) 65 | return 0; 66 | 67 | if (out_size != 32) 68 | return 0; 69 | 70 | input64 = in + 64; 71 | recid = in[63] - 27; 72 | 73 | /* higher bytes of V should be zero */ 74 | static const char z31 [31]; 75 | if (memcmp (z31, in + 32, 31)) 76 | return 0; 77 | 78 | if (recid < 0 || recid > 3) 79 | return 0; 80 | 81 | if (!secp256k1_ecdsa_recoverable_signature_parse_compact 82 | (ctx, &rsig, input64, recid)) 83 | return 0; 84 | 85 | if (!secp256k1_ecdsa_recover (ctx, &pubkey, &rsig, in)) 86 | return 0; 87 | 88 | if (!secp256k1_ec_pubkey_serialize 89 | (ctx, pubkey_hex, &hexlen, &pubkey, SECP256K1_EC_UNCOMPRESSED)) 90 | return 0; 91 | 92 | if (sha3_256 (out, 32, pubkey_hex + 1, 64)) 93 | return 0; 94 | 95 | memset (out, 0, 12); 96 | 97 | return 1; 98 | } 99 | 100 | int ethjet_blake2(uint8_t *in, size_t in_size, 101 | uint8_t *out, size_t out_size) { 102 | uint32_t rounds = in[0] << 24 | in[1] << 16 | in[2] << 8 | in[3]; 103 | unsigned char f = in[212]; 104 | uint64_t *h = (uint64_t *)&in[4]; 105 | uint64_t *m = (uint64_t *)&in[68]; 106 | uint64_t *t = (uint64_t *)&in[196]; 107 | 108 | blake2b_compress(h, m, t, f, rounds); 109 | 110 | memcpy(out, h, out_size); 111 | 112 | return 1; 113 | } 114 | 115 | int 116 | ethjet (struct ethjet_context *ctx, 117 | enum ethjet_operation op, 118 | uint8_t *in, size_t in_size, 119 | uint8_t *out, size_t out_size) 120 | { 121 | switch (op) { 122 | case ETHJET_ECRECOVER: 123 | return ethjet_ecrecover (ctx->ec, in, in_size, out, out_size); 124 | break; 125 | 126 | case ETHJET_EXAMPLE: 127 | return ethjet_example (ctx, in, in_size, out, out_size); 128 | 129 | case ETHJET_ECADD: 130 | return ethjet_ecadd (in, in_size, out, out_size); 131 | 132 | case ETHJET_ECMUL: 133 | return ethjet_ecmul (in, in_size, out, out_size); 134 | 135 | case ETHJET_ECPAIRING: 136 | return ethjet_ecpairing (in, in_size, out, out_size); 137 | 138 | case ETHJET_BLAKE2: 139 | return ethjet_blake2 (in, in_size, out, out_size); 140 | 141 | default: 142 | return 0; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /ethjet/ethjet.h: -------------------------------------------------------------------------------- 1 | #ifndef LIBETHJET_H 2 | #define LIBETHJET_H 3 | 4 | #include 5 | 6 | #include 7 | 8 | struct ethjet_context 9 | { 10 | secp256k1_context *ec; 11 | }; 12 | 13 | enum ethjet_operation 14 | { 15 | ETHJET_ECRECOVER = 1, 16 | ETHJET_ECADD = 6, 17 | ETHJET_ECMUL = 7, 18 | ETHJET_ECPAIRING = 8, 19 | ETHJET_BLAKE2 = 9, 20 | ETHJET_EXAMPLE = 0xdeadbeef, 21 | }; 22 | 23 | struct ethjet_context * 24 | ethjet_init (); 25 | 26 | void 27 | ethjet_free (struct ethjet_context *ctx); 28 | 29 | int ethjet (struct ethjet_context *ctx, 30 | enum ethjet_operation op, 31 | uint8_t *in, size_t in_size, 32 | uint8_t *out, size_t out_size); 33 | 34 | #endif 35 | -------------------------------------------------------------------------------- /ethjet/tinykeccak.c: -------------------------------------------------------------------------------- 1 | /** libkeccak-tiny 2 | * 3 | * A single-file implementation of SHA-3 and SHAKE. 4 | * 5 | * Implementor: David Leon Gil 6 | * License: CC0, attribution kindly requested. Blame taken too, 7 | * but not liability. 8 | */ 9 | 10 | #include "tinykeccak.h" 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | /******** The Keccak-f[1600] permutation ********/ 18 | 19 | /*** Constants. ***/ 20 | static const uint8_t rho[24] = \ 21 | { 1, 3, 6, 10, 15, 21, 22 | 28, 36, 45, 55, 2, 14, 23 | 27, 41, 56, 8, 25, 43, 24 | 62, 18, 39, 61, 20, 44}; 25 | static const uint8_t pi[24] = \ 26 | {10, 7, 11, 17, 18, 3, 27 | 5, 16, 8, 21, 24, 4, 28 | 15, 23, 19, 13, 12, 2, 29 | 20, 14, 22, 9, 6, 1}; 30 | static const uint64_t RC[24] = \ 31 | {1ULL, 0x8082ULL, 0x800000000000808aULL, 0x8000000080008000ULL, 32 | 0x808bULL, 0x80000001ULL, 0x8000000080008081ULL, 0x8000000000008009ULL, 33 | 0x8aULL, 0x88ULL, 0x80008009ULL, 0x8000000aULL, 34 | 0x8000808bULL, 0x800000000000008bULL, 0x8000000000008089ULL, 0x8000000000008003ULL, 35 | 0x8000000000008002ULL, 0x8000000000000080ULL, 0x800aULL, 0x800000008000000aULL, 36 | 0x8000000080008081ULL, 0x8000000000008080ULL, 0x80000001ULL, 0x8000000080008008ULL}; 37 | 38 | /*** Helper macros to unroll the permutation. ***/ 39 | #define rol(x, s) (((x) << s) | ((x) >> (64 - s))) 40 | #define REPEAT6(e) e e e e e e 41 | #define REPEAT24(e) REPEAT6(e e e e) 42 | #define REPEAT5(e) e e e e e 43 | #define FOR5(v, s, e) \ 44 | v = 0; \ 45 | REPEAT5(e; v += s;) 46 | 47 | /*** Keccak-f[1600] ***/ 48 | static inline void keccakf(void* state) { 49 | uint64_t* a = (uint64_t*)state; 50 | uint64_t b[5] = {0}; 51 | uint64_t t = 0; 52 | uint8_t x, y; 53 | 54 | for (int i = 0; i < 24; i++) { 55 | // Theta 56 | FOR5(x, 1, 57 | b[x] = 0; 58 | FOR5(y, 5, 59 | b[x] ^= a[x + y]; )) 60 | FOR5(x, 1, 61 | FOR5(y, 5, 62 | a[y + x] ^= b[(x + 4) % 5] ^ rol(b[(x + 1) % 5], 1); )) 63 | // Rho and pi 64 | t = a[1]; 65 | x = 0; 66 | REPEAT24(b[0] = a[pi[x]]; 67 | a[pi[x]] = rol(t, rho[x]); 68 | t = b[0]; 69 | x++; ) 70 | // Chi 71 | FOR5(y, 72 | 5, 73 | FOR5(x, 1, 74 | b[x] = a[y + x];) 75 | FOR5(x, 1, 76 | a[y + x] = b[x] ^ ((~b[(x + 1) % 5]) & b[(x + 2) % 5]); )) 77 | // Iota 78 | a[0] ^= RC[i]; 79 | } 80 | } 81 | 82 | /******** The FIPS202-defined functions. ********/ 83 | 84 | /*** Some helper macros. ***/ 85 | 86 | #define _(S) do { S } while (0) 87 | #define FOR(i, ST, L, S) \ 88 | _(for (size_t i = 0; i < L; i += ST) { S; }) 89 | #define mkapply_ds(NAME, S) \ 90 | static inline void NAME(uint8_t* dst, \ 91 | const uint8_t* src, \ 92 | size_t len) { \ 93 | FOR(i, 1, len, S); \ 94 | } 95 | #define mkapply_sd(NAME, S) \ 96 | static inline void NAME(const uint8_t* src, \ 97 | uint8_t* dst, \ 98 | size_t len) { \ 99 | FOR(i, 1, len, S); \ 100 | } 101 | 102 | mkapply_ds(xorin, dst[i] ^= src[i]) // xorin 103 | mkapply_sd(setout, dst[i] = src[i]) // setout 104 | 105 | #define P keccakf 106 | #define Plen 200 107 | 108 | // Fold P*F over the full blocks of an input. 109 | #define foldP(I, L, F) \ 110 | while (L >= rate) { \ 111 | F(a, I, rate); \ 112 | P(a); \ 113 | I += rate; \ 114 | L -= rate; \ 115 | } 116 | 117 | /** The sponge-based hash construction. **/ 118 | static inline int hash(uint8_t* out, size_t outlen, 119 | const uint8_t* in, size_t inlen, 120 | size_t rate, uint8_t delim) { 121 | if ((out == NULL) || ((in == NULL) && inlen != 0) || (rate >= Plen)) { 122 | return -1; 123 | } 124 | uint8_t a[Plen] = {0}; 125 | // Absorb input. 126 | foldP(in, inlen, xorin); 127 | // Xor in the DS and pad frame. 128 | a[inlen] ^= delim; 129 | a[rate - 1] ^= 0x80; 130 | // Xor in the last block. 131 | xorin(a, in, inlen); 132 | // Apply P 133 | P(a); 134 | // Squeeze output. 135 | foldP(out, outlen, setout); 136 | setout(a, out, outlen); 137 | memset(a, 0, 200); 138 | return 0; 139 | } 140 | 141 | #define defsha3(bits) \ 142 | int sha3_##bits(uint8_t* out, size_t outlen, \ 143 | const uint8_t* in, size_t inlen) { \ 144 | if (outlen > (bits/8)) { \ 145 | return -1; \ 146 | } \ 147 | return hash(out, outlen, in, inlen, 200 - (bits / 4), 0x01); \ 148 | } 149 | 150 | /*** FIPS202 SHA3 FOFs ***/ 151 | defsha3(256) 152 | defsha3(512) 153 | -------------------------------------------------------------------------------- /ethjet/tinykeccak.h: -------------------------------------------------------------------------------- 1 | #ifndef KECCAK_H 2 | #define KECCAK_H 3 | 4 | #include 5 | #include 6 | 7 | int sha3_256 (uint8_t *out, size_t out_size, 8 | const uint8_t *in, size_t in_size); 9 | 10 | #endif 11 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "ethereum-tests": { 4 | "flake": false, 5 | "locked": { 6 | "lastModified": 1698656397, 7 | "narHash": "sha256-1zhBaJ3X5kQ94qv9Yz12/3d83w3jNP6OtzjH+ykK8sg=", 8 | "owner": "ethereum", 9 | "repo": "tests", 10 | "rev": "428f218d7d6f4a52544e12684afbfe6e2882ffbf", 11 | "type": "github" 12 | }, 13 | "original": { 14 | "owner": "ethereum", 15 | "ref": "v13", 16 | "repo": "tests", 17 | "type": "github" 18 | } 19 | }, 20 | "flake-utils": { 21 | "inputs": { 22 | "systems": "systems" 23 | }, 24 | "locked": { 25 | "lastModified": 1710146030, 26 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", 27 | "owner": "numtide", 28 | "repo": "flake-utils", 29 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", 30 | "type": "github" 31 | }, 32 | "original": { 33 | "owner": "numtide", 34 | "repo": "flake-utils", 35 | "type": "github" 36 | } 37 | }, 38 | "flake-utils_2": { 39 | "locked": { 40 | "lastModified": 1644229661, 41 | "narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=", 42 | "owner": "numtide", 43 | "repo": "flake-utils", 44 | "rev": "3cecb5b042f7f209c56ffd8371b2711a290ec797", 45 | "type": "github" 46 | }, 47 | "original": { 48 | "owner": "numtide", 49 | "repo": "flake-utils", 50 | "type": "github" 51 | } 52 | }, 53 | "flake-utils_3": { 54 | "inputs": { 55 | "systems": "systems_2" 56 | }, 57 | "locked": { 58 | "lastModified": 1710146030, 59 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", 60 | "owner": "numtide", 61 | "repo": "flake-utils", 62 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", 63 | "type": "github" 64 | }, 65 | "original": { 66 | "owner": "numtide", 67 | "repo": "flake-utils", 68 | "type": "github" 69 | } 70 | }, 71 | "forge-std": { 72 | "flake": false, 73 | "locked": { 74 | "lastModified": 1715903882, 75 | "narHash": "sha256-Uqr0ZwCnQL9ShWxIgG/ci/5ukGyuqt2n+C0GFKlJiho=", 76 | "owner": "foundry-rs", 77 | "repo": "forge-std", 78 | "rev": "52715a217dc51d0de15877878ab8213f6cbbbab5", 79 | "type": "github" 80 | }, 81 | "original": { 82 | "owner": "foundry-rs", 83 | "repo": "forge-std", 84 | "type": "github" 85 | } 86 | }, 87 | "foundry": { 88 | "inputs": { 89 | "flake-utils": "flake-utils_2", 90 | "nixpkgs": "nixpkgs" 91 | }, 92 | "locked": { 93 | "lastModified": 1724145005, 94 | "narHash": "sha256-fTvalF9fSKWJj/HJWtHQ8DrMR1nBH1NV1w/x+O4M/Zw=", 95 | "owner": "shazow", 96 | "repo": "foundry.nix", 97 | "rev": "47f8ae49275eeff9bf0526d45e3c1f76723bb5d3", 98 | "type": "github" 99 | }, 100 | "original": { 101 | "owner": "shazow", 102 | "repo": "foundry.nix", 103 | "rev": "47f8ae49275eeff9bf0526d45e3c1f76723bb5d3", 104 | "type": "github" 105 | } 106 | }, 107 | "nixpkgs": { 108 | "locked": { 109 | "lastModified": 1666753130, 110 | "narHash": "sha256-Wff1dGPFSneXJLI2c0kkdWTgxnQ416KE6X4KnFkgPYQ=", 111 | "owner": "NixOS", 112 | "repo": "nixpkgs", 113 | "rev": "f540aeda6f677354f1e7144ab04352f61aaa0118", 114 | "type": "github" 115 | }, 116 | "original": { 117 | "id": "nixpkgs", 118 | "type": "indirect" 119 | } 120 | }, 121 | "nixpkgs_2": { 122 | "locked": { 123 | "lastModified": 1733229606, 124 | "narHash": "sha256-FLYY5M0rpa5C2QAE3CKLYAM6TwbKicdRK6qNrSHlNrE=", 125 | "owner": "nixos", 126 | "repo": "nixpkgs", 127 | "rev": "566e53c2ad750c84f6d31f9ccb9d00f823165550", 128 | "type": "github" 129 | }, 130 | "original": { 131 | "owner": "nixos", 132 | "ref": "nixpkgs-unstable", 133 | "repo": "nixpkgs", 134 | "type": "github" 135 | } 136 | }, 137 | "root": { 138 | "inputs": { 139 | "ethereum-tests": "ethereum-tests", 140 | "flake-utils": "flake-utils", 141 | "forge-std": "forge-std", 142 | "foundry": "foundry", 143 | "nixpkgs": "nixpkgs_2", 144 | "solc-pkgs": "solc-pkgs", 145 | "solidity": "solidity" 146 | } 147 | }, 148 | "solc-macos-amd64-list-json": { 149 | "flake": false, 150 | "locked": { 151 | "narHash": "sha256-Prwz95BgMHcWd72VwVbcH17LsV9f24K2QMcUiWUQZzI=", 152 | "type": "file", 153 | "url": "https://github.com/ethereum/solc-bin/raw/f743ca7/macosx-amd64/list.json" 154 | }, 155 | "original": { 156 | "type": "file", 157 | "url": "https://github.com/ethereum/solc-bin/raw/f743ca7/macosx-amd64/list.json" 158 | } 159 | }, 160 | "solc-pkgs": { 161 | "inputs": { 162 | "flake-utils": "flake-utils_3", 163 | "nixpkgs": [ 164 | "nixpkgs" 165 | ], 166 | "solc-macos-amd64-list-json": "solc-macos-amd64-list-json" 167 | }, 168 | "locked": { 169 | "lastModified": 1724145339, 170 | "narHash": "sha256-z8pLkpdsAA0At1ofQd6KNmrxpBuUT9OKTlCDqJDW1GI=", 171 | "owner": "hellwolf", 172 | "repo": "solc.nix", 173 | "rev": "9630767051bfefd552c6858c5df141368338b077", 174 | "type": "github" 175 | }, 176 | "original": { 177 | "owner": "hellwolf", 178 | "repo": "solc.nix", 179 | "type": "github" 180 | } 181 | }, 182 | "solidity": { 183 | "flake": false, 184 | "locked": { 185 | "lastModified": 1716280767, 186 | "narHash": "sha256-cvRFeJkiaYPA+SoboEZhvH5sHDmmcMNR62OoKuEOWRg=", 187 | "owner": "ethereum", 188 | "repo": "solidity", 189 | "rev": "8a97fa7a1db1ec509221ead6fea6802c684ee887", 190 | "type": "github" 191 | }, 192 | "original": { 193 | "owner": "ethereum", 194 | "repo": "solidity", 195 | "rev": "8a97fa7a1db1ec509221ead6fea6802c684ee887", 196 | "type": "github" 197 | } 198 | }, 199 | "systems": { 200 | "locked": { 201 | "lastModified": 1681028828, 202 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 203 | "owner": "nix-systems", 204 | "repo": "default", 205 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 206 | "type": "github" 207 | }, 208 | "original": { 209 | "owner": "nix-systems", 210 | "repo": "default", 211 | "type": "github" 212 | } 213 | }, 214 | "systems_2": { 215 | "locked": { 216 | "lastModified": 1681028828, 217 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 218 | "owner": "nix-systems", 219 | "repo": "default", 220 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 221 | "type": "github" 222 | }, 223 | "original": { 224 | "owner": "nix-systems", 225 | "repo": "default", 226 | "type": "github" 227 | } 228 | } 229 | }, 230 | "root": "root", 231 | "version": 7 232 | } 233 | -------------------------------------------------------------------------------- /hie.yaml: -------------------------------------------------------------------------------- 1 | cradle: 2 | cabal: 3 | - path: "./src" 4 | component: "lib:hevm" 5 | - path: "./bench" 6 | component: "bench" 7 | - path: "./cli" 8 | component: "exe:hevm" 9 | - path: "./test/test.hs" 10 | component: "test:test" 11 | - path: "./test/" 12 | component: "lib:test-utils" 13 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # hevm 2 | hevm is an implementation of the Ethereum virtual machine (EVM) made for 3 | symbolic execution, equivalence checking, and (symbolic) unit testing of smart 4 | contracts. `hevm` can symbolically execute smart contracts, perform symbolic 5 | equivalence testing, and run arbitrary EVM code. In particular, it can run 6 | [Forge](https://book.getfoundry.sh/forge/writing-tests) test suites in a 7 | symbolic way, thereby being much more thorough than fuzz testing. 8 | 9 | ## Documentation & Support 10 | User facing documentation can be found in the [hevm book](https://hevm.dev/). 11 | We have a public matrix chat room 12 | [here](https://matrix.to/#/%23hevm%3Amatrix.org). 13 | 14 | ## Installing via Static Binaries 15 | Static binaries for x86 linux and macos are available for each 16 | [release](https://github.com/ethereum/hevm/releases). These binaries expect to be able to find the 17 | following programs on `PATH`: 18 | - `z3` 19 | - (optionally) `cvc5` 20 | - (optionally) `bitwuzla` 21 | 22 | ## Installing via nix 23 | hevm nix package is available in 24 | [nixpkgs](https://search.nixos.org/packages?channel=unstable&show=haskellPackages.hevm), 25 | and can be installed via: 26 | - flakes: `nix profile install nixpkgs#haskellPackages.hevm` 27 | - legacy: `nix-env -iA haskellPackages.hevm` 28 | 29 | hevm flake can be installed directly from the `main` branch of this repo via the following command: 30 | ```plain 31 | nix profile install github:ethereum/hevm 32 | ``` 33 | 34 | ## Development 35 | We use `nix` to manage project dependencies. To start hacking on hevm you should first [install 36 | nix](https://nixos.org/download.html). 37 | 38 | Once nix is installed you can run `nix develop` from the repo root to enter a development shell 39 | containing all required dev dependencies. 40 | 41 | Once in the shell you can use the usual `cabal` commands to build and test hevm: 42 | ```plain 43 | $ cabal run hevm -- test --root myproject # run the cli 44 | $ cabal run test # run the tests 45 | $ cabal repl test # enter the repl for the test.sh 46 | $ cabal run ethereum-tests # run the ethereum standard tests 47 | 48 | # run the cli binary with profiling enabled 49 | $ cabal run --enable-profiling hevm -- +RTS -s -p -RTS 50 | ``` 51 | 52 | ## History 53 | `hevm` was originally developed as part of the 54 | [dapptools](https://github.com/dapphub/dapptools/) project, and was forked to 55 | this repo by the formal methods team at the Ethereum Foundation in August 2022. 56 | 57 | -------------------------------------------------------------------------------- /src/EVM/Assembler.hs: -------------------------------------------------------------------------------- 1 | {-| 2 | Module : Assembler 3 | Description : Assembler for EVM opcodes used in the HEVM symbolic checker 4 | -} 5 | module EVM.Assembler where 6 | 7 | import EVM.Expr qualified as Expr 8 | import EVM.Op 9 | import EVM.Types 10 | 11 | import Data.Vector (Vector) 12 | import Data.Vector qualified as V 13 | 14 | assemble :: [Op] -> Vector (Expr Byte) 15 | assemble os = V.fromList $ concatMap go os 16 | where 17 | go :: Op -> [Expr Byte] 18 | go = \case 19 | OpStop -> [LitByte 0x00] 20 | OpAdd -> [LitByte 0x01] 21 | OpMul -> [LitByte 0x02] 22 | OpSub -> [LitByte 0x03] 23 | OpDiv -> [LitByte 0x04] 24 | OpSdiv -> [LitByte 0x05] 25 | OpMod -> [LitByte 0x06] 26 | OpSmod -> [LitByte 0x07] 27 | OpAddmod -> [LitByte 0x08] 28 | OpMulmod -> [LitByte 0x09] 29 | OpExp -> [LitByte 0x0A] 30 | OpSignextend -> [LitByte 0x0B] 31 | OpLt -> [LitByte 0x10] 32 | OpGt -> [LitByte 0x11] 33 | OpSlt -> [LitByte 0x12] 34 | OpSgt -> [LitByte 0x13] 35 | OpEq -> [LitByte 0x14] 36 | OpIszero -> [LitByte 0x15] 37 | OpAnd -> [LitByte 0x16] 38 | OpOr -> [LitByte 0x17] 39 | OpXor -> [LitByte 0x18] 40 | OpNot -> [LitByte 0x19] 41 | OpByte -> [LitByte 0x1A] 42 | OpShl -> [LitByte 0x1B] 43 | OpShr -> [LitByte 0x1C] 44 | OpSar -> [LitByte 0x1D] 45 | OpSha3 -> [LitByte 0x20] 46 | OpAddress -> [LitByte 0x30] 47 | OpBalance -> [LitByte 0x31] 48 | OpOrigin -> [LitByte 0x32] 49 | OpCaller -> [LitByte 0x33] 50 | OpCallvalue -> [LitByte 0x34] 51 | OpCalldataload -> [LitByte 0x35] 52 | OpCalldatasize -> [LitByte 0x36] 53 | OpCalldatacopy -> [LitByte 0x37] 54 | OpCodesize -> [LitByte 0x38] 55 | OpCodecopy -> [LitByte 0x39] 56 | OpGasprice -> [LitByte 0x3A] 57 | OpExtcodesize -> [LitByte 0x3B] 58 | OpExtcodecopy -> [LitByte 0x3C] 59 | OpReturndatasize -> [LitByte 0x3D] 60 | OpReturndatacopy -> [LitByte 0x3E] 61 | OpExtcodehash -> [LitByte 0x3F] 62 | OpBlockhash -> [LitByte 0x40] 63 | OpCoinbase -> [LitByte 0x41] 64 | OpTimestamp -> [LitByte 0x42] 65 | OpNumber -> [LitByte 0x43] 66 | OpPrevRandao -> [LitByte 0x44] 67 | OpGaslimit -> [LitByte 0x45] 68 | OpChainid -> [LitByte 0x46] 69 | OpSelfbalance -> [LitByte 0x47] 70 | OpBaseFee -> [LitByte 0x48] 71 | OpBlobhash -> [LitByte 0x49] 72 | OpBlobBaseFee -> [LitByte 0x4A] 73 | OpPop -> [LitByte 0x50] 74 | OpMcopy -> [LitByte 0x5e] 75 | OpMload -> [LitByte 0x51] 76 | OpMstore -> [LitByte 0x52] 77 | OpMstore8 -> [LitByte 0x53] 78 | OpSload -> [LitByte 0x54] 79 | OpSstore -> [LitByte 0x55] 80 | OpTload -> [LitByte 0x5c] 81 | OpTstore -> [LitByte 0x5d] 82 | OpJump -> [LitByte 0x56] 83 | OpJumpi -> [LitByte 0x57] 84 | OpPc -> [LitByte 0x58] 85 | OpMsize -> [LitByte 0x59] 86 | OpGas -> [LitByte 0x5A] 87 | OpJumpdest -> [LitByte 0x5B] 88 | OpCreate -> [LitByte 0xF0] 89 | OpCall -> [LitByte 0xF1] 90 | OpStaticcall -> [LitByte 0xFA] 91 | OpCallcode -> [LitByte 0xF2] 92 | OpReturn -> [LitByte 0xF3] 93 | OpDelegatecall -> [LitByte 0xF4] 94 | OpCreate2 -> [LitByte 0xF5] 95 | OpRevert -> [LitByte 0xFD] 96 | OpSelfdestruct -> [LitByte 0xFF] 97 | OpDup n -> 98 | if 1 <= n && n <= 16 99 | then [LitByte (0x80 + (n - 1))] 100 | else internalError $ "invalid argument to OpDup: " <> show n 101 | OpSwap n -> 102 | if 1 <= n && n <= 16 103 | then [LitByte (0x90 + (n - 1))] 104 | else internalError $ "invalid argument to OpSwap: " <> show n 105 | OpLog n -> 106 | if 0 <= n && n <= 4 107 | then [LitByte (0xA0 + n)] 108 | else internalError $ "invalid argument to OpLog: " <> show n 109 | -- we just always assemble OpPush into PUSH32 110 | OpPush wrd -> (LitByte 0x7f) : [Expr.indexWord (Lit i) wrd | i <- [0..31]] 111 | OpPush0 -> [LitByte 0x5f] 112 | OpUnknown o -> [LitByte o] 113 | -------------------------------------------------------------------------------- /src/EVM/CSE.hs: -------------------------------------------------------------------------------- 1 | {- | 2 | Module: EVM.CSE 3 | Description: Common subexpression elimination for Expr ast 4 | -} 5 | 6 | module EVM.CSE (BufEnv, StoreEnv, eliminateExpr, eliminateProps) where 7 | 8 | import Control.Monad.State 9 | import Data.Map (Map) 10 | import Data.Map qualified as Map 11 | 12 | import EVM.Traversals 13 | import EVM.Types 14 | 15 | -- maps expressions to variable names 16 | data BuilderState = BuilderState 17 | { bufs :: Map (Expr Buf) Int 18 | , stores :: Map (Expr Storage) Int 19 | , count :: Int 20 | } 21 | deriving (Show) 22 | 23 | type BufEnv = Map Int (Expr Buf) 24 | type StoreEnv = Map Int (Expr Storage) 25 | 26 | initState :: BuilderState 27 | initState = BuilderState 28 | { bufs = mempty 29 | , stores = mempty 30 | , count = 0 31 | } 32 | 33 | 34 | go :: Expr a -> State BuilderState (Expr a) 35 | go = \case 36 | -- buffers 37 | e@(WriteWord {}) -> do 38 | s <- get 39 | case Map.lookup e s.bufs of 40 | Just v -> pure $ GVar (BufVar v) 41 | Nothing -> do 42 | let 43 | next = s.count 44 | bs' = Map.insert e next s.bufs 45 | put $ s{bufs=bs', count=next+1} 46 | pure $ GVar (BufVar next) 47 | e@(WriteByte {}) -> do 48 | s <- get 49 | case Map.lookup e s.bufs of 50 | Just v -> pure $ GVar (BufVar v) 51 | Nothing -> do 52 | let 53 | next = s.count 54 | bs' = Map.insert e next s.bufs 55 | put $ s{bufs=bs', count=next+1} 56 | pure $ GVar (BufVar next) 57 | e@(CopySlice {}) -> do 58 | s <- get 59 | case Map.lookup e s.bufs of 60 | Just v -> pure $ GVar (BufVar v) 61 | Nothing -> do 62 | let 63 | next = s.count 64 | bs' = Map.insert e next s.bufs 65 | put $ s{count=next+1, bufs=bs'} 66 | pure $ GVar (BufVar next) 67 | -- storage 68 | e@(SStore {}) -> do 69 | s <- get 70 | case Map.lookup e s.stores of 71 | Just v -> pure $ GVar (StoreVar v) 72 | Nothing -> do 73 | let 74 | next = s.count 75 | ss' = Map.insert e next s.stores 76 | put $ s{count=next+1, stores=ss'} 77 | pure $ GVar (StoreVar next) 78 | e -> pure e 79 | 80 | invertKeyVal :: forall a. Map a Int -> Map Int a 81 | invertKeyVal = Map.fromList . map (\(x, y) -> (y, x)) . Map.toList 82 | 83 | -- | Common subexpression elimination pass for Expr 84 | eliminateExpr' :: Expr a -> State BuilderState (Expr a) 85 | eliminateExpr' e = mapExprM go e 86 | 87 | eliminateExpr :: Expr a -> (Expr a, BufEnv, StoreEnv) 88 | eliminateExpr e = 89 | let (e', st) = runState (eliminateExpr' e) initState in 90 | (e', invertKeyVal st.bufs, invertKeyVal st.stores) 91 | 92 | -- | Common subexpression elimination pass for Prop 93 | eliminateProp' :: Prop -> State BuilderState Prop 94 | eliminateProp' prop = mapPropM go prop 95 | 96 | -- | Common subexpression elimination pass for list of Prop 97 | eliminateProps' :: [Prop] -> State BuilderState [Prop] 98 | eliminateProps' props = mapM eliminateProp' props 99 | 100 | 101 | -- | Common subexpression elimination pass for list of Prop 102 | eliminateProps :: [Prop] -> ([Prop], BufEnv, StoreEnv) 103 | eliminateProps props = 104 | let (props', st) = runState (eliminateProps' props) initState in 105 | (props', invertKeyVal st.bufs, invertKeyVal st.stores) 106 | -------------------------------------------------------------------------------- /src/EVM/CheatsTH.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE TemplateHaskell #-} 2 | 3 | module EVM.CheatsTH where 4 | 5 | import EVM.ABI 6 | import EVM.Types (internalError) 7 | 8 | import Data.ByteString.Char8 (pack) 9 | import Data.Map.Strict qualified as Map 10 | import Data.Vector qualified as V 11 | 12 | import Language.Haskell.TH 13 | import Language.Haskell.TH.Syntax 14 | 15 | liftByteString :: String -> Q Exp 16 | liftByteString txt = AppE (VarE 'pack) <$> lift txt 17 | 18 | liftAbiType :: AbiType -> Q Exp 19 | liftAbiType AbiBoolType = [| AbiBool |] 20 | liftAbiType (AbiUIntType n) = [| AbiUInt $(lift n) |] 21 | liftAbiType (AbiIntType n) = [| AbiInt $(lift n) |] 22 | liftAbiType AbiAddressType = [| AbiAddress |] 23 | liftAbiType (AbiBytesType n) = [| AbiBytes $(lift n) |] 24 | liftAbiType AbiStringType = [| AbiString |] 25 | liftAbiType AbiBytesDynamicType = [| AbiBytesDynamic |] 26 | liftAbiType _ = internalError "unimplemented" 27 | 28 | envReadSingleCheat :: String -> Q Exp 29 | envReadSingleCheat sigString = [| 30 | \wrap convert -> 31 | action $sigL $ 32 | \sig input -> case decodeBuf [AbiStringType] input of 33 | CAbi [AbiString variable] -> let 34 | varStr = toString variable 35 | cont value = continueOnce $ do 36 | either' (convert value) frameRevert $ \v -> 37 | frameReturn $ wrap v 38 | in do 39 | vm <- get 40 | case Map.lookup varStr vm.osEnv of 41 | Just v -> cont v 42 | Nothing -> query (PleaseReadEnv varStr cont) 43 | _ -> vmError (BadCheatCode (sigString <> " parameter decoding failed") sig) 44 | |] 45 | where 46 | sigL = liftByteString sigString 47 | 48 | envReadMultipleCheat :: String -> AbiType -> Q Exp 49 | envReadMultipleCheat sigString arrType = [| 50 | \convert -> 51 | action $sigL $ 52 | \sig input -> case decodeBuf [AbiStringType, AbiStringType] input of 53 | CAbi [AbiString variable, AbiString delimiter] -> let 54 | (varStr, delimStr) = (toString variable, toString delimiter) 55 | cont value = continueOnce $ do 56 | let (errors, values) = partitionEithers $ map convert $ splitOn delimStr value 57 | case errors of 58 | [] -> do 59 | let result = AbiTuple $ V.fromList [AbiArrayDynamic $arrTypeL $ V.fromList $ map $wrapL values] 60 | frameReturn result 61 | (e:_) -> frameRevert e 62 | in do 63 | vm <- get 64 | case Map.lookup varStr vm.osEnv of 65 | Just v -> cont v 66 | Nothing -> query (PleaseReadEnv varStr cont) 67 | _ -> vmError (BadCheatCode (sigString <> " parameter decoding failed") sig) 68 | |] 69 | where 70 | sigL = liftByteString sigString 71 | arrTypeL = liftData arrType 72 | wrapL = liftAbiType arrType 73 | -------------------------------------------------------------------------------- /src/EVM/Concrete.hs: -------------------------------------------------------------------------------- 1 | module EVM.Concrete where 2 | 3 | import Prelude hiding (Word) 4 | import EVM.RLP 5 | import EVM.Types 6 | 7 | import Data.ByteString (ByteString) 8 | import Data.ByteString qualified as BS 9 | import Witch (unsafeInto, into) 10 | 11 | byteStringSliceWithDefaultZeroes :: Int -> Int -> ByteString -> ByteString 12 | byteStringSliceWithDefaultZeroes offset size bs = 13 | if size == 0 14 | then "" 15 | else 16 | let bs' = BS.take size (BS.drop offset bs) 17 | in bs' <> BS.replicate (size - BS.length bs') 0 18 | 19 | sliceMemory :: W256 -> W256 -> ByteString -> ByteString 20 | sliceMemory o s = 21 | byteStringSliceWithDefaultZeroes (unsafeInto o) (unsafeInto s) 22 | 23 | writeMemory :: ByteString -> W256 -> W256 -> W256 -> ByteString -> ByteString 24 | writeMemory bs1 n src dst bs0 = 25 | let 26 | (a, b) = BS.splitAt (unsafeInto dst) bs0 27 | a' = BS.replicate (unsafeInto dst - BS.length a) 0 28 | -- sliceMemory should work for both cases, but we are using 256 bit 29 | -- words, whereas ByteString is only defined up to 64 bit. For large n, 30 | -- src, dst this will cause problems (often in GeneralStateTests). 31 | -- Later we could reimplement ByteString for 256 bit arguments. 32 | c = if src > unsafeInto (BS.length bs1) 33 | then BS.replicate (unsafeInto n) 0 34 | else sliceMemory src n bs1 35 | b' = BS.drop (unsafeInto n) b 36 | in 37 | a <> a' <> c <> b' 38 | 39 | createAddress :: Addr -> W64 -> Expr EAddr 40 | createAddress a n = LitAddr . unsafeInto . keccak' . rlpList $ [rlpAddrFull a, rlpWord256 (into n)] 41 | 42 | create2Address :: Addr -> W256 -> ByteString -> Expr EAddr 43 | create2Address a s b = LitAddr $ unsafeInto $ keccak' $ mconcat 44 | [BS.singleton 0xff, word160Bytes a, word256Bytes s, word256Bytes $ keccak' b] 45 | -------------------------------------------------------------------------------- /src/EVM/Dapp.hs: -------------------------------------------------------------------------------- 1 | module EVM.Dapp where 2 | 3 | import EVM.ABI 4 | import EVM.Concrete 5 | import EVM.Solidity 6 | import EVM.Types 7 | import EVM.Expr (maybeLitByteSimp, maybeLitWordSimp) 8 | 9 | import Control.Arrow ((>>>), second) 10 | import Data.Aeson (Value) 11 | import Data.ByteString (ByteString) 12 | import Data.ByteString qualified as BS 13 | import Data.List (find, sort) 14 | import Data.Map (Map) 15 | import Data.Map qualified as Map 16 | import Data.Maybe (mapMaybe) 17 | import Data.Sequence qualified as Seq 18 | import Data.Text (Text, isPrefixOf, pack) 19 | import Data.Text.Encoding (encodeUtf8) 20 | import Data.Vector qualified as V 21 | import Optics.Core 22 | import Witch (unsafeInto) 23 | 24 | data DappInfo = DappInfo 25 | { root :: FilePath 26 | , solcByName :: Map Text SolcContract 27 | , solcByHash :: Map W256 (CodeType, SolcContract) 28 | , solcByCode :: [(Code, SolcContract)] -- for contracts with `immutable` vars. 29 | , sources :: SourceCache 30 | , abiMap :: Map FunctionSelector Method 31 | , eventMap :: Map W256 Event 32 | , errorMap :: Map W256 SolError 33 | , astIdMap :: Map Int Value 34 | , astSrcMap :: SrcMap -> Maybe Value 35 | } 36 | 37 | -- | bytecode modulo immutables, to identify contracts 38 | data Code = Code 39 | { raw :: ByteString 40 | , immutableLocations :: [Reference] 41 | } 42 | deriving Show 43 | 44 | data DappContext = DappContext 45 | { info :: DappInfo 46 | , contracts :: Map (Expr EAddr) Contract 47 | , labels :: Map Addr Text 48 | } 49 | 50 | dappInfo :: FilePath -> BuildOutput -> DappInfo 51 | dappInfo root (BuildOutput (Contracts cs) sources) = 52 | let 53 | solcs = Map.elems cs 54 | astIds = astIdMap $ snd <$> Map.toList sources.asts 55 | immutables = filter ((/=) mempty . (.immutableReferences)) solcs 56 | 57 | in DappInfo 58 | { root = root 59 | , sources = sources 60 | , solcByName = cs 61 | , solcByHash = 62 | let 63 | f g k = Map.fromList [(g x, (k, x)) | x <- solcs] 64 | in 65 | mappend 66 | (f (.runtimeCodehash) Runtime) 67 | (f (.creationCodehash) Creation) 68 | -- contracts with immutable locations can't be id by hash 69 | , solcByCode = 70 | [(Code x.runtimeCode (concat $ Map.elems x.immutableReferences), x) | x <- immutables] 71 | -- Sum up the ABI maps from all the contracts. 72 | , abiMap = mconcat (map (.abiMap) solcs) 73 | , eventMap = mconcat (map (.eventMap) solcs) 74 | , errorMap = mconcat (map (.errorMap) solcs) 75 | 76 | , astIdMap = astIds 77 | , astSrcMap = astSrcMap astIds 78 | } 79 | 80 | emptyDapp :: DappInfo 81 | emptyDapp = dappInfo "" mempty 82 | 83 | -- Dapp unit tests are detected by searching within abi methods 84 | -- that begin with "check" or "prove", that are in a contract with 85 | -- the "IS_TEST()" abi marker, for a given regular expression. 86 | -- 87 | -- The regex is matched on the full test method name, including path 88 | -- and contract, i.e. "path/to/file.sol:TestContract.test_name()". 89 | 90 | unitTestMarkerAbi :: FunctionSelector 91 | unitTestMarkerAbi = abiKeccak (encodeUtf8 "IS_TEST()") 92 | 93 | mkSig :: Text -> Method -> Maybe Sig 94 | mkSig prefix method 95 | | prefix `isPrefixOf` testname = Just (Sig testname argtypes) 96 | | otherwise = Nothing 97 | where 98 | testname = method.name 99 | argtypes = snd <$> method.inputs 100 | 101 | findUnitTests :: Text -> Text -> ([SolcContract] -> [(Text, [Sig])]) 102 | findUnitTests prefix match = 103 | concatMap $ \c -> 104 | case Map.lookup unitTestMarkerAbi c.abiMap of 105 | Nothing -> [] 106 | Just _ -> 107 | let testNames = unitTestMethodsFiltered prefix (regexMatches match) c 108 | in [(c.contractName, testNames) | not (BS.null c.runtimeCode) && not (null testNames)] 109 | 110 | unitTestMethodsFiltered :: Text -> (Text -> Bool) -> (SolcContract -> [Sig]) 111 | unitTestMethodsFiltered prefix matcher c = 112 | let testName (Sig n _) = c.contractName <> "." <> n 113 | in filter (matcher . testName) (unitTestMethods prefix c) 114 | 115 | unitTestMethods :: Text -> SolcContract -> [Sig] 116 | unitTestMethods prefix = 117 | (.abiMap) 118 | >>> Map.elems 119 | >>> mapMaybe (mkSig prefix) 120 | 121 | traceSrcMap :: DappInfo -> Trace -> Maybe SrcMap 122 | traceSrcMap dapp trace = srcMap dapp trace.contract trace.opIx 123 | 124 | srcMap :: DappInfo -> Contract -> Int -> Maybe SrcMap 125 | srcMap dapp contr opIndex = do 126 | sol <- findSrc contr dapp 127 | case contr.code of 128 | UnknownCode _ -> Nothing 129 | InitCode _ _ -> 130 | Seq.lookup opIndex sol.creationSrcmap 131 | RuntimeCode _ -> 132 | Seq.lookup opIndex sol.runtimeSrcmap 133 | 134 | findSrc :: Contract -> DappInfo -> Maybe SolcContract 135 | findSrc c dapp = do 136 | hash <- maybeLitWordSimp c.codehash 137 | case Map.lookup hash dapp.solcByHash of 138 | Just (_, v) -> Just v 139 | Nothing -> lookupCode c.code dapp 140 | 141 | 142 | lookupCode :: ContractCode -> DappInfo -> Maybe SolcContract 143 | lookupCode (UnknownCode _) _ = Nothing 144 | lookupCode (InitCode c _) a = 145 | snd <$> Map.lookup (keccak' (stripBytecodeMetadata c)) a.solcByHash 146 | lookupCode (RuntimeCode (ConcreteRuntimeCode c)) a = 147 | case snd <$> Map.lookup (keccak' (stripBytecodeMetadata c)) a.solcByHash of 148 | Just x -> pure x 149 | Nothing -> snd <$> find (compareCode c . fst) a.solcByCode 150 | lookupCode (RuntimeCode (SymbolicRuntimeCode c)) a = let 151 | code = BS.pack $ mapMaybe maybeLitByteSimp $ V.toList c 152 | in case snd <$> Map.lookup (keccak' (stripBytecodeMetadata code)) a.solcByHash of 153 | Just x -> pure x 154 | Nothing -> snd <$> find (compareCode code . fst) a.solcByCode 155 | 156 | compareCode :: ByteString -> Code -> Bool 157 | compareCode raw (Code template locs) = 158 | let holes' = sort [(start, len) | (Reference start len) <- locs] 159 | insert loc len' bs = writeMemory (BS.replicate len' 0) (unsafeInto len') 0 (unsafeInto loc) bs 160 | refined = foldr (\(start, len) acc -> insert start len acc) raw holes' 161 | in BS.length raw == BS.length template && template == refined 162 | 163 | showTraceLocation :: DappInfo -> Trace -> Either Text Text 164 | showTraceLocation dapp trace = 165 | case traceSrcMap dapp trace of 166 | Nothing -> Left "" 167 | Just sm -> 168 | case srcMapCodePos dapp.sources sm of 169 | Nothing -> Left "" 170 | Just (fileName, lineIx) -> 171 | Right (pack fileName <> ":" <> pack (show lineIx)) 172 | 173 | srcMapCodePos :: SourceCache -> SrcMap -> Maybe (FilePath, Int) 174 | srcMapCodePos cache sm = 175 | fmap (second f) $ cache.files ^? ix sm.file 176 | where 177 | f v = BS.count 0xa (BS.take sm.offset v) + 1 178 | 179 | srcMapCode :: SourceCache -> SrcMap -> Maybe ByteString 180 | srcMapCode cache sm = 181 | fmap f $ cache.files ^? ix sm.file 182 | where 183 | f (_, v) = BS.take (min 80 sm.length) (BS.drop sm.offset v) 184 | -------------------------------------------------------------------------------- /src/EVM/Effects.hs: -------------------------------------------------------------------------------- 1 | {-| 2 | Module : Effects 3 | Description : Domain specific effects 4 | 5 | This module contains custom app specific mtl style effects for hevm 6 | These are written in the style of the ReaderT over IO pattern [1]. 7 | Right now we only have a single `ReadConfig` effect, but over time hope to 8 | migrate most usages of IO into custom effects here. 9 | 10 | This framework would allow us to have multiple interpretations for effects 11 | (e.g. a pure version for tests), but for now we interpret everything in IO 12 | only. 13 | 14 | [1]: https://www.fpcomplete.com/blog/readert-design-pattern/ 15 | -} 16 | 17 | module EVM.Effects where 18 | 19 | import Control.Monad.Reader 20 | import Control.Monad.IO.Unlift 21 | import Data.Text (Text) 22 | import Data.Text.IO qualified as T 23 | import System.IO (stderr) 24 | 25 | 26 | -- Abstract Effects -------------------------------------------------------------------------------- 27 | -- Here we define the abstract interface for the effects that we wish to model 28 | 29 | 30 | -- Read from global config 31 | class Monad m => ReadConfig m where 32 | readConfig :: m Config 33 | 34 | data Config = Config 35 | { dumpQueries :: Bool 36 | , dumpExprs :: Bool 37 | , dumpEndStates :: Bool 38 | , debug :: Bool 39 | , dumpTrace :: Bool 40 | , numCexFuzz :: Integer 41 | -- Used to debug fuzzer in test.hs. It disables the SMT solver 42 | -- and uses the fuzzer ONLY to try to find a counterexample. 43 | -- Returns Unknown if the Cex cannot be found via fuzzing 44 | , onlyCexFuzz :: Bool 45 | , decomposeStorage :: Bool 46 | , promiseNoReent :: Bool 47 | , maxBufSize :: Int 48 | , maxWidth :: Int 49 | , maxDepth :: Maybe Int 50 | , verb :: Int 51 | } 52 | deriving (Show, Eq) 53 | 54 | defaultConfig :: Config 55 | defaultConfig = Config 56 | { dumpQueries = False 57 | , dumpExprs = False 58 | , dumpEndStates = False 59 | , debug = False 60 | , dumpTrace = False 61 | , numCexFuzz = 10 62 | , onlyCexFuzz = False 63 | , decomposeStorage = True 64 | , promiseNoReent = False 65 | , maxBufSize = 64 66 | , maxWidth = 100 67 | , maxDepth = Nothing 68 | , verb = 0 69 | } 70 | 71 | -- Write to the console 72 | class Monad m => TTY m where 73 | writeOutput :: Text -> m () 74 | writeErr :: Text -> m () 75 | 76 | -- IO Interpretation ------------------------------------------------------------------------------- 77 | 78 | 79 | newtype Env = Env 80 | { config :: Config 81 | } 82 | 83 | defaultEnv :: Env 84 | defaultEnv = Env { config = defaultConfig } 85 | 86 | instance Monad m => ReadConfig (ReaderT Env m) where 87 | readConfig = do 88 | e <- ask 89 | pure e.config 90 | 91 | instance (Monad m, MonadIO m) => TTY (ReaderT Env m) where 92 | writeOutput txt = liftIO $ T.putStrLn txt 93 | writeErr txt = liftIO $ T.hPutStr stderr txt 94 | 95 | runEnv :: Env -> ReaderT Env m a -> m a 96 | runEnv e a = runReaderT a e 97 | 98 | 99 | -- Helpful Aliases --------------------------------------------------------------------------------- 100 | 101 | 102 | type App m = (MonadUnliftIO m, ReadConfig m, TTY m) 103 | 104 | runApp :: ReaderT Env m a -> m a 105 | runApp = runEnv defaultEnv 106 | -------------------------------------------------------------------------------- /src/EVM/Exec.hs: -------------------------------------------------------------------------------- 1 | module EVM.Exec where 2 | 3 | import EVM hiding (createAddress) 4 | import EVM.Concrete (createAddress) 5 | import EVM.FeeSchedule (feeSchedule) 6 | import EVM.Types 7 | 8 | import Control.Monad.Trans.State.Strict (get, State) 9 | import Data.ByteString (ByteString) 10 | import Data.Maybe (isNothing) 11 | import Optics.Core 12 | import Control.Monad.ST (ST) 13 | import EVM.Effects (Config) 14 | 15 | ethrunAddress :: Addr 16 | ethrunAddress = Addr 0x00a329c0648769a73afac7f9381e08fb43dbea72 17 | 18 | vmForEthrunCreation :: VMOps t => ByteString -> ST s (VM t s) 19 | vmForEthrunCreation creationCode = 20 | (makeVm $ VMOpts 21 | { contract = initialContract (InitCode creationCode mempty) 22 | , otherContracts = [] 23 | , calldata = mempty 24 | , value = Lit 0 25 | , baseState = EmptyBase 26 | , address = createAddress ethrunAddress 1 27 | , caller = LitAddr ethrunAddress 28 | , origin = LitAddr ethrunAddress 29 | , coinbase = LitAddr 0 30 | , number = Lit 0 31 | , timestamp = Lit 0 32 | , blockGaslimit = 0 33 | , gasprice = 0 34 | , prevRandao = 42069 35 | , gas = toGas 0xffffffffffffffff 36 | , gaslimit = 0xffffffffffffffff 37 | , baseFee = 0 38 | , priorityFee = 0 39 | , maxCodeSize = 0xffffffff 40 | , schedule = feeSchedule 41 | , chainId = 1 42 | , create = False 43 | , txAccessList = mempty 44 | , allowFFI = False 45 | , freshAddresses = 0 46 | , beaconRoot = 0 47 | }) <&> set (#env % #contracts % at (LitAddr ethrunAddress)) 48 | (Just (initialContract (RuntimeCode (ConcreteRuntimeCode "")))) 49 | 50 | exec :: (VMOps t) => Config -> EVM t s (VMResult t s) 51 | exec conf = do 52 | vm <- get 53 | case vm.result of 54 | Nothing -> exec1 conf >> exec conf 55 | Just r -> pure r 56 | 57 | run :: (VMOps t) => Config -> EVM t s (VM t s) 58 | run conf = do 59 | vm <- get 60 | case vm.result of 61 | Nothing -> exec1 conf >> run conf 62 | Just _ -> pure vm 63 | 64 | execWhile :: (VM t s -> Bool) -> State (VM t s) Int 65 | execWhile p = go 0 66 | where 67 | go i = do 68 | vm <- get 69 | if p vm && isNothing vm.result 70 | then do 71 | go $! (i + 1) 72 | else 73 | pure i 74 | -------------------------------------------------------------------------------- /src/EVM/FeeSchedule.hs: -------------------------------------------------------------------------------- 1 | module EVM.FeeSchedule where 2 | 3 | data FeeSchedule n = FeeSchedule 4 | { g_zero :: n 5 | , g_base :: n 6 | , g_verylow :: n 7 | , g_low :: n 8 | , g_mid :: n 9 | , g_high :: n 10 | , g_extcode :: n 11 | , g_balance :: n 12 | , g_sload :: n 13 | , g_jumpdest :: n 14 | , g_sset :: n 15 | , g_sreset :: n 16 | , r_sclear :: n 17 | , g_selfdestruct :: n 18 | , g_selfdestruct_newaccount :: n 19 | , r_selfdestruct :: n 20 | , g_create :: n 21 | , g_codedeposit :: n 22 | , g_call :: n 23 | , g_callvalue :: n 24 | , g_callstipend :: n 25 | , g_newaccount :: n 26 | , g_exp :: n 27 | , g_expbyte :: n 28 | , g_memory :: n 29 | , g_txcreate :: n 30 | , g_txdatazero :: n 31 | , g_txdatanonzero :: n 32 | , g_transaction :: n 33 | , g_log :: n 34 | , g_logdata :: n 35 | , g_logtopic :: n 36 | , g_sha3 :: n 37 | , g_sha3word :: n 38 | , g_initcodeword :: n 39 | , g_copy :: n 40 | , g_blockhash :: n 41 | , g_extcodehash :: n 42 | , g_quaddivisor :: n 43 | , g_ecadd :: n 44 | , g_ecmul :: n 45 | , g_pairing_point :: n 46 | , g_pairing_base :: n 47 | , g_fround :: n 48 | , r_block :: n 49 | , g_cold_sload :: n 50 | , g_cold_account_access :: n 51 | , g_warm_storage_read :: n 52 | , g_access_list_address :: n 53 | , g_access_list_storage_key :: n 54 | } deriving Show 55 | 56 | feeSchedule :: Num n => FeeSchedule n 57 | feeSchedule = FeeSchedule 58 | { g_zero = 0 59 | , g_base = 2 60 | , g_verylow = 3 61 | , g_low = 5 62 | , g_mid = 8 63 | , g_high = 10 64 | , g_extcode = 2600 65 | , g_balance = 2600 66 | , g_sload = 100 67 | , g_jumpdest = 1 68 | , g_sset = 20000 69 | , g_sreset = 2900 70 | , r_sclear = 15000 71 | , g_selfdestruct = 5000 72 | , g_selfdestruct_newaccount = 25000 73 | , r_selfdestruct = 24000 74 | , g_create = 32000 75 | , g_codedeposit = 200 76 | , g_call = 2600 77 | , g_callvalue = 9000 78 | , g_callstipend = 2300 79 | , g_newaccount = 25000 80 | , g_exp = 10 81 | , g_expbyte = 50 82 | , g_memory = 3 83 | , g_txcreate = 32000 84 | , g_txdatazero = 4 85 | , g_txdatanonzero = 16 86 | , g_transaction = 21000 87 | , g_log = 375 88 | , g_logdata = 8 89 | , g_logtopic = 375 90 | , g_sha3 = 30 91 | , g_sha3word = 6 92 | , g_initcodeword = 2 93 | , g_copy = 3 94 | , g_blockhash = 20 95 | , g_extcodehash = 2600 96 | , g_quaddivisor = 20 97 | , g_ecadd = 150 98 | , g_ecmul = 6000 99 | , g_pairing_point = 34000 100 | , g_pairing_base = 45000 101 | , g_fround = 1 102 | , r_block = 2000000000000000000 103 | , g_cold_sload = 2100 104 | , g_cold_account_access = 2600 105 | , g_warm_storage_read = 100 106 | , g_access_list_address = 2400 107 | , g_access_list_storage_key = 1900 108 | } 109 | -------------------------------------------------------------------------------- /src/EVM/Keccak.hs: -------------------------------------------------------------------------------- 1 | {- | 2 | Module: EVM.Keccak 3 | Description: Expr passes to determine Keccak assumptions 4 | -} 5 | module EVM.Keccak (keccakAssumptions, keccakCompute) where 6 | 7 | import Control.Monad.State 8 | import Data.Set (Set) 9 | import Data.Set qualified as Set 10 | import Data.List (tails) 11 | 12 | import EVM.Traversals 13 | import EVM.Types 14 | import EVM.Expr 15 | 16 | 17 | newtype KeccakStore = KeccakStore 18 | { keccakExprs :: Set (Expr EWord) } 19 | deriving (Show) 20 | 21 | initState :: KeccakStore 22 | initState = KeccakStore { keccakExprs = Set.empty } 23 | 24 | keccakFinder :: forall a. Expr a -> State KeccakStore () 25 | keccakFinder = \case 26 | e@(Keccak _) -> modify (\s -> s{keccakExprs=Set.insert e s.keccakExprs}) 27 | _ -> pure () 28 | 29 | findKeccakExpr :: forall a. Expr a -> State KeccakStore () 30 | findKeccakExpr e = mapExprM_ keccakFinder e 31 | 32 | findKeccakProp :: Prop -> State KeccakStore () 33 | findKeccakProp p = mapPropM_ keccakFinder p 34 | 35 | findKeccakPropsExprs :: [Prop] -> [Expr Buf] -> [Expr Storage]-> State KeccakStore () 36 | findKeccakPropsExprs ps bufs stores = do 37 | mapM_ findKeccakProp ps 38 | mapM_ findKeccakExpr bufs 39 | mapM_ findKeccakExpr stores 40 | 41 | 42 | uniquePairs :: [a] -> [(a,a)] 43 | uniquePairs xs = [(x,y) | (x:ys) <- Data.List.tails xs, y <- ys] 44 | 45 | minProp :: Expr EWord -> Prop 46 | minProp k@(Keccak _) = PGT k (Lit 256) 47 | minProp _ = internalError "expected keccak expression" 48 | 49 | injProp :: (Expr EWord, Expr EWord) -> Prop 50 | injProp (k1@(Keccak b1), k2@(Keccak b2)) = 51 | PImpl (PEq k1 k2) ((b1 .== b2) .&& (bufLength b1 .== bufLength b2)) 52 | injProp _ = internalError "expected keccak expression" 53 | 54 | 55 | -- Takes a list of props, find all keccak occurrences and generates two kinds of assumptions: 56 | -- 1. Minimum output value: That the output of the invocation is greater than 57 | -- 256 (needed to avoid spurious counterexamples due to storage collisions 58 | -- with solidity mappings & value type storage slots) 59 | -- 2. Injectivity: That keccak is an injective function (we avoid quantifiers 60 | -- here by making this claim for each unique pair of keccak invocations 61 | -- discovered in the input expressions) 62 | keccakAssumptions :: [Prop] -> [Expr Buf] -> [Expr Storage] -> [Prop] 63 | keccakAssumptions ps bufs stores = injectivity <> minValue <> minDiffOfPairs 64 | where 65 | (_, st) = runState (findKeccakPropsExprs ps bufs stores) initState 66 | 67 | keccakPairs = uniquePairs (Set.toList st.keccakExprs) 68 | injectivity = map injProp keccakPairs 69 | minValue = map minProp (Set.toList st.keccakExprs) 70 | minDiffOfPairs = map minDistance keccakPairs 71 | where 72 | minDistance :: (Expr EWord, Expr EWord) -> Prop 73 | minDistance (ka@(Keccak a), kb@(Keccak b)) = PImpl ((a ./= b) .|| (bufLength a ./= bufLength b)) (PAnd req1 req2) 74 | where 75 | req1 = (PGEq (Sub ka kb) (Lit 256)) 76 | req2 = (PGEq (Sub kb ka) (Lit 256)) 77 | minDistance _ = internalError "expected Keccak expression" 78 | 79 | compute :: forall a. Expr a -> Set Prop 80 | compute = \case 81 | e@(Keccak buf) -> do 82 | let b = simplify buf 83 | case keccak b of 84 | lit@(Lit _) -> Set.singleton (PEq lit e) 85 | _ -> Set.empty 86 | _ -> Set.empty 87 | 88 | computeKeccakExpr :: forall a. Expr a -> Set Prop 89 | computeKeccakExpr e = foldExpr compute Set.empty e 90 | 91 | computeKeccakProp :: Prop -> Set Prop 92 | computeKeccakProp p = foldProp compute Set.empty p 93 | 94 | keccakCompute :: [Prop] -> [Expr Buf] -> [Expr Storage] -> [Prop] 95 | keccakCompute ps buf stores = 96 | Set.toList $ 97 | (foldMap computeKeccakProp ps) <> 98 | (foldMap computeKeccakExpr buf) <> 99 | (foldMap computeKeccakExpr stores) 100 | -------------------------------------------------------------------------------- /src/EVM/Precompiled.hs: -------------------------------------------------------------------------------- 1 | module EVM.Precompiled (execute) where 2 | 3 | import Data.ByteString (ByteString) 4 | import Data.ByteString qualified as BS 5 | 6 | import Foreign.C 7 | import Foreign.Ptr 8 | import Foreign.ForeignPtr 9 | 10 | import System.IO.Unsafe 11 | 12 | -- | Opaque representation of the C library's context struct. 13 | data EthjetContext 14 | 15 | foreign import ccall unsafe "ethjet_init" 16 | ethjet_init :: IO (Ptr EthjetContext) 17 | foreign import ccall unsafe "ðjet_free" 18 | ethjet_free :: FunPtr (Ptr EthjetContext -> IO ()) 19 | foreign import ccall unsafe "ethjet" 20 | ethjet 21 | :: Ptr EthjetContext -- initialized context 22 | -> CInt -- operation 23 | -> Ptr CChar -> CInt -- input 24 | -> Ptr CChar -> CInt -- output 25 | -> IO CInt -- 1 if good 26 | 27 | -- Lazy evaluation ensures this context is only initialized once, 28 | -- and `unsafePerformIO` in such situations is a common pattern. 29 | -- 30 | -- We use a "foreign pointer" annotated with a finalizer. 31 | globalContext :: ForeignPtr EthjetContext 32 | {-# NOINLINE globalContext #-} 33 | globalContext = 34 | unsafePerformIO $ 35 | ethjet_init >>= newForeignPtr ethjet_free 36 | 37 | -- | Run a given precompiled contract using the C library. 38 | execute 39 | :: Int -- ^ The number of the precompiled contract 40 | -> ByteString -- ^ The input buffer 41 | -> Int -- ^ The desired output size 42 | -> Maybe ByteString -- ^ Hopefully, the output buffer 43 | execute contract input outputSize = 44 | 45 | -- This code looks messy because of the pointer handling, 46 | -- but it's actually simple. 47 | -- 48 | -- We use `unsafePerformIO` because the contracts are pure. 49 | 50 | unsafePerformIO . BS.useAsCStringLen input $ 51 | \(inputPtr, inputSize) -> do 52 | outputForeignPtr <- mallocForeignPtrBytes outputSize 53 | withForeignPtr outputForeignPtr $ \outputPtr -> do 54 | status <- 55 | withForeignPtr globalContext $ \contextPtr -> 56 | -- Finally, we can invoke the C library. 57 | ethjet contextPtr (fromIntegral contract) 58 | inputPtr (fromIntegral inputSize) 59 | outputPtr (fromIntegral outputSize) 60 | 61 | case status of 62 | 1 -> Just <$> BS.packCStringLen (outputPtr, outputSize) 63 | _ -> pure Nothing 64 | -------------------------------------------------------------------------------- /src/EVM/RLP.hs: -------------------------------------------------------------------------------- 1 | module EVM.RLP where 2 | 3 | import EVM.Types 4 | import Data.Bits (shiftR) 5 | import Data.ByteString (ByteString) 6 | import Data.ByteString qualified as BS 7 | import Witch (into, unsafeInto) 8 | 9 | data RLP = BS ByteString | List [RLP] deriving Eq 10 | 11 | instance Show RLP where 12 | show (BS str) = show (ByteStringS str) 13 | show (List list) = show list 14 | 15 | slice :: Int -> Int -> ByteString -> ByteString 16 | slice offset size bs = BS.take size $ BS.drop offset bs 17 | 18 | -- helper function returning (the length of the prefix, the length of the content, isList boolean, optimal boolean) 19 | itemInfo :: ByteString -> (Int, Int, Bool, Bool) 20 | itemInfo bs | bs == mempty = (0, 0, False, False) 21 | | otherwise = case BS.head bs of 22 | x | 0 <= x && x < 128 -> (0, 1, False, True) -- directly encoded byte 23 | x | 128 <= x && x < 184 -> (1, into x - 128, False, (BS.length bs /= 2) || (127 < (BS.head $ BS.drop 1 bs))) -- short string 24 | x | 184 <= x && x < 192 -> (1 + pre, len, False, (len > 55) && BS.head (BS.drop 1 bs) /= 0) -- long string 25 | where pre = into $ x - 183 26 | -- TODO: unsafeInto fails: cabal run test -- -p 'rlp' --quickcheck-replay=413899 27 | len = fromIntegral $ word $ slice 1 pre bs 28 | x | 192 <= x && x < 248 -> (1, into $ x - 192, True, True) -- short list 29 | x -> (1 + pre, len, True, (len > 55) && BS.head (BS.drop 1 bs) /= 0) -- long list 30 | where pre = into $ x - 247 31 | -- TODO: unsafeInto fails: cabal run test -- -p 'rlp' --quickcheck-replay=146332 32 | len = fromIntegral $ word $ slice 1 pre bs 33 | 34 | rlpdecode :: ByteString -> Maybe RLP 35 | rlpdecode bs = 36 | if optimal && pre + len == BS.length bs then 37 | if isList then do 38 | items <- mapM (\(s, e) -> rlpdecode $ slice s e content) (rlplengths content 0 len) 39 | Just (List items) 40 | else 41 | Just (BS content) 42 | else Nothing 43 | where (pre, len, isList, optimal) = itemInfo bs 44 | content = BS.drop pre bs 45 | 46 | rlplengths :: ByteString -> Int -> Int -> [(Int,Int)] 47 | rlplengths bs acc top = 48 | if acc < top then 49 | let (pre, len, _, _) = itemInfo bs 50 | in (acc, pre + len) : rlplengths (BS.drop (pre + len) bs) (acc + pre + len) top 51 | else [] 52 | 53 | rlpencode :: RLP -> ByteString 54 | rlpencode (BS bs) = if BS.length bs == 1 && BS.head bs < 128 then bs 55 | else encodeLen 128 bs 56 | rlpencode (List items) = encodeLen 192 (mconcat $ map rlpencode items) 57 | 58 | encodeLen :: Int -> ByteString -> ByteString 59 | encodeLen offset bs = 60 | if BS.length bs <= 55 then prefix (BS.length bs) <> bs 61 | else prefix lenLen <> lenBytes <> bs 62 | where 63 | lenBytes = asBE $ BS.length bs 64 | prefix n = BS.singleton $ unsafeInto $ offset + n 65 | lenLen = BS.length lenBytes + 55 66 | 67 | rlpList :: [RLP] -> ByteString 68 | rlpList n = rlpencode $ List n 69 | 70 | octets :: W256 -> ByteString 71 | octets x = 72 | BS.pack $ dropWhile (== 0) [fromIntegral (shiftR x (8 * i)) | i <- reverse [0..31]] 73 | 74 | octetsFull :: Int -> W256 -> ByteString 75 | octetsFull n x = 76 | BS.pack $ [fromIntegral (shiftR x (8 * i)) | i <- reverse [0..n]] 77 | 78 | octets160 :: Addr -> ByteString 79 | octets160 x = 80 | BS.pack $ dropWhile (== 0) [fromIntegral (shiftR x (8 * i)) | i <- reverse [0..19]] 81 | 82 | rlpWord256 :: W256 -> RLP 83 | rlpWord256 0 = BS mempty 84 | rlpWord256 n = BS $ octets n 85 | 86 | rlpWordFull :: W256 -> RLP 87 | rlpWordFull = BS . octetsFull 31 88 | 89 | rlpAddrFull :: Addr -> RLP 90 | rlpAddrFull = BS . octetsFull 19 . into 91 | 92 | rlpWord160 :: Addr -> RLP 93 | rlpWord160 0 = BS mempty 94 | rlpWord160 n = BS $ octets160 n 95 | -------------------------------------------------------------------------------- /src/EVM/Sign.hs: -------------------------------------------------------------------------------- 1 | {-| 2 | Module : Helper functions to sign a transaction and derive address from 3 | Description : for the EVM given a secret key 4 | -} 5 | module EVM.Sign where 6 | 7 | import Data.ByteString qualified as BS 8 | import Data.Maybe (fromMaybe) 9 | import Data.Word 10 | import Crypto.Hash qualified as Crypto 11 | import Crypto.PubKey.ECC.ECDSA (signDigestWith, PrivateKey(..), Signature(..)) 12 | import Crypto.PubKey.ECC.Types (getCurveByName, CurveName(..), Point(..)) 13 | import Crypto.PubKey.ECC.Generate (generateQ) 14 | import Witch (unsafeInto) 15 | 16 | import EVM.ABI (encodeAbiValue, AbiValue(..)) 17 | import EVM.Types 18 | import EVM.Expr (exprToAddr) 19 | import EVM.Precompiled 20 | 21 | -- Given a secret key, generates the address 22 | deriveAddr :: Integer -> Maybe Addr 23 | deriveAddr sk = 24 | case pubPoint of 25 | PointO -> Nothing 26 | Point x y -> 27 | -- See yellow paper #286 28 | let pub = BS.concat [ encodeInt x, encodeInt y ] 29 | addr = Lit . W256 . word256 . BS.drop 12 . BS.take 32 . keccakBytes $ pub 30 | in exprToAddr addr 31 | where 32 | curve = getCurveByName SEC_p256k1 33 | pubPoint = generateQ curve sk 34 | encodeInt = encodeAbiValue . AbiUInt 256 . fromInteger 35 | 36 | sign :: W256 -> Integer -> (Word8, W256, W256) 37 | sign hash sk = (v, r, s) 38 | where 39 | -- setup curve params 40 | curve = getCurveByName SEC_p256k1 41 | priv = PrivateKey curve sk 42 | digest = fromMaybe 43 | (internalError $ "could produce a digest from " <> show hash) 44 | (Crypto.digestFromByteString (word256Bytes hash)) 45 | 46 | -- sign message 47 | sig = ethsign priv digest 48 | r = unsafeInto $ sign_r sig 49 | s = unsafeInto lowS 50 | 51 | -- this is a little bit sad, but cryptonite doesn't give us back a v value 52 | -- so we compute it by guessing one, and then seeing if that gives us the right answer from ecrecover 53 | v = if ecrec 28 r s hash == deriveAddr sk 54 | then 28 55 | else 27 56 | 57 | -- we always use the lower S value to conform with EIP2 (re: ECDSA transaction malleability) 58 | -- https://eips.ethereum.org/EIPS/eip-2 59 | secpOrder = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 :: Integer 60 | lowS = if sign_s sig > secpOrder `div` 2 61 | then secpOrder - sign_s sig 62 | else sign_s sig 63 | 64 | -- | We don't want to introduce the machinery needed to sign with a random nonce, 65 | -- so we just use the same nonce every time (420). This is obviously very 66 | -- insecure, but fine for testing purposes. 67 | ethsign :: PrivateKey -> Crypto.Digest Crypto.Keccak_256 -> Signature 68 | ethsign sk digest = go 420 69 | where 70 | go k = case signDigestWith k sk digest of 71 | Nothing -> go (k + 1) 72 | Just sig -> sig 73 | 74 | ecrec :: W256 -> W256 -> W256 -> W256 -> Maybe Addr 75 | ecrec v r s e = unsafeInto . word <$> EVM.Precompiled.execute 1 input 32 76 | where input = BS.concat (word256Bytes <$> [e, v, r, s]) 77 | -------------------------------------------------------------------------------- /src/EVM/Stepper.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE ScopedTypeVariables #-} 2 | 3 | module EVM.Stepper 4 | ( Action (..) 5 | , Stepper 6 | , exec 7 | , execFully 8 | , run 9 | , runFully 10 | , wait 11 | , fork 12 | , evm 13 | , enter 14 | , interpret 15 | ) 16 | where 17 | 18 | -- This module is an abstract definition of EVM steppers. 19 | -- Steppers can be run as TTY debuggers or as CLI test runners. 20 | -- 21 | -- The implementation uses the operational monad pattern 22 | -- as the framework for monadic interpretation. 23 | 24 | import Control.Monad.IO.Class 25 | import Control.Monad.Operational (Program, ProgramViewT(..), ProgramView, singleton, view) 26 | import Control.Monad.ST (stToIO, RealWorld) 27 | import Control.Monad.State.Strict (execStateT, runStateT, get) 28 | import Data.Text (Text) 29 | 30 | import EVM qualified 31 | import EVM.Effects 32 | import EVM.Exec qualified 33 | import EVM.Fetch qualified as Fetch 34 | import EVM.Types 35 | 36 | -- | The instruction type of the operational monad 37 | data Action t s a where 38 | -- | Keep executing until an intermediate result is reached 39 | Exec :: Action t s (VMResult t s) 40 | -- | Embed a VM state transformation 41 | EVM :: EVM t s a -> Action t s a 42 | -- | Wait for a query to be resolved 43 | Wait :: Query t s -> Action t s () 44 | -- | Two things can happen 45 | Fork :: RunBoth s -> Action Symbolic s () 46 | -- | Many (>2) things can happen 47 | ForkMany :: RunAll s -> Action Symbolic s () 48 | 49 | -- | Type alias for an operational monad of @Action@ 50 | type Stepper t s a = Program (Action t s) a 51 | 52 | -- Singleton actions 53 | 54 | exec :: Stepper t s (VMResult t s) 55 | exec = singleton Exec 56 | 57 | run :: Stepper t s (VM t s) 58 | run = exec >> evm get 59 | 60 | wait :: Query t s -> Stepper t s () 61 | wait = singleton . Wait 62 | 63 | fork :: RunBoth s -> Stepper Symbolic s () 64 | fork = singleton . Fork 65 | 66 | forkMany :: RunAll s -> Stepper Symbolic s () 67 | forkMany = singleton . ForkMany 68 | 69 | evm :: EVM t s a -> Stepper t s a 70 | evm = singleton . EVM 71 | 72 | -- | Run the VM until final result, resolving all queries 73 | execFully :: Stepper Concrete s (Either EvmError (Expr Buf)) 74 | execFully = 75 | exec >>= \case 76 | HandleEffect (Query q) -> 77 | wait q >> execFully 78 | VMFailure x -> 79 | pure (Left x) 80 | VMSuccess x -> 81 | pure (Right x) 82 | 83 | -- | Run the VM until its final state 84 | runFully :: Stepper t s (VM t s) 85 | runFully = do 86 | vm <- run 87 | case vm.result of 88 | Nothing -> internalError "should not occur" 89 | Just (HandleEffect (Query q)) -> 90 | wait q >> runFully 91 | Just (HandleEffect (RunBoth q)) -> 92 | fork q >> runFully 93 | Just (HandleEffect (RunAll q)) -> 94 | forkMany q >> runFully 95 | Just _ -> 96 | pure vm 97 | 98 | enter :: Text -> Stepper t s () 99 | enter t = evm (EVM.pushTrace (EntryTrace t)) 100 | 101 | interpret 102 | :: forall m a . (App m) 103 | => Fetch.Fetcher Concrete m RealWorld 104 | -> VM Concrete RealWorld 105 | -> Stepper Concrete RealWorld a 106 | -> m a 107 | interpret fetcher vm = eval . view 108 | where 109 | eval :: ProgramView (Action Concrete RealWorld) a -> m a 110 | eval (Return x) = pure x 111 | eval (action :>>= k) = 112 | case action of 113 | Exec -> do 114 | conf <- readConfig 115 | (r, vm') <- liftIO $ stToIO $ runStateT (EVM.Exec.exec conf) vm 116 | interpret fetcher vm' (k r) 117 | Wait q -> do 118 | m <- fetcher q 119 | vm' <- liftIO $ stToIO $ execStateT m vm 120 | interpret fetcher vm' (k ()) 121 | EVM m -> do 122 | (r, vm') <- liftIO $ stToIO $ runStateT m vm 123 | interpret fetcher vm' (k r) 124 | -------------------------------------------------------------------------------- /test/BlockchainTests.hs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Test.Tasty 4 | import EVM.Effects 5 | import EVM.Test.BlockchainTests qualified as BlockchainTests 6 | 7 | testEnv :: Env 8 | testEnv = Env { config = defaultConfig } 9 | 10 | main :: IO () 11 | main = do 12 | tests <- runEnv testEnv BlockchainTests.prepareTests 13 | defaultMain tests 14 | -------------------------------------------------------------------------------- /test/EVM/Test/Utils.hs: -------------------------------------------------------------------------------- 1 | module EVM.Test.Utils where 2 | 3 | import Data.Text (Text) 4 | import Data.List (isInfixOf) 5 | import GHC.IO.Exception (IOErrorType(..)) 6 | import GHC.Natural 7 | import Paths_hevm qualified as Paths 8 | import System.Directory 9 | import System.FilePath (()) 10 | import System.IO.Temp 11 | import System.Process 12 | import System.Exit 13 | import System.IO.Error (mkIOError) 14 | 15 | import EVM.Dapp (dappInfo, emptyDapp) 16 | import EVM.Fetch (RpcInfo) 17 | import EVM.Solidity 18 | import EVM.Solvers 19 | import EVM.UnitTest 20 | import Control.Monad.ST (RealWorld) 21 | import Control.Monad.IO.Unlift 22 | import Control.Monad.Catch (MonadMask) 23 | import EVM.Effects 24 | import Data.Maybe (fromMaybe) 25 | import EVM.Types (internalError) 26 | import System.Environment (lookupEnv) 27 | 28 | -- Returns tuple of (No cex, No warnings) 29 | runSolidityTestCustom 30 | :: (MonadMask m, App m) 31 | => FilePath -> Text -> Maybe Natural -> Maybe Integer -> Bool -> RpcInfo -> ProjectType -> m (Bool, Bool) 32 | runSolidityTestCustom testFile match timeout maxIter ffiAllowed rpcinfo projectType = do 33 | withSystemTempDirectory "dapp-test" $ \root -> do 34 | (compile projectType root testFile) >>= \case 35 | Left e -> liftIO $ do 36 | putStrLn e 37 | internalError $ "Error compiling test file " <> show testFile <> " in directory " 38 | <> show root <> " using project type " <> show projectType 39 | Right bo@(BuildOutput contracts _) -> do 40 | withSolvers Bitwuzla 3 1 timeout $ \solvers -> do 41 | opts <- liftIO $ testOpts solvers root (Just bo) match maxIter ffiAllowed rpcinfo 42 | unitTest opts contracts 43 | 44 | -- Returns tuple of (No cex, No warnings) 45 | runSolidityTest 46 | :: (MonadMask m, App m) 47 | => FilePath -> Text -> m (Bool, Bool) 48 | runSolidityTest testFile match = runSolidityTestCustom testFile match Nothing Nothing True Nothing Foundry 49 | 50 | testOpts :: SolverGroup -> FilePath -> Maybe BuildOutput -> Text -> Maybe Integer -> Bool -> RpcInfo -> IO (UnitTestOptions RealWorld) 51 | testOpts solvers root buildOutput match maxIter allowFFI rpcinfo = do 52 | let srcInfo = maybe emptyDapp (dappInfo root) buildOutput 53 | 54 | params <- paramsFromRpc rpcinfo 55 | 56 | pure UnitTestOptions 57 | { solvers = solvers 58 | , rpcInfo = rpcinfo 59 | , maxIter = maxIter 60 | , askSmtIters = 1 61 | , smtTimeout = Nothing 62 | , match = match 63 | , prefix = "prove" 64 | , testParams = params 65 | , dapp = srcInfo 66 | , ffiAllowed = allowFFI 67 | , checkFailBit = False 68 | } 69 | 70 | processFailedException :: String -> String -> [String] -> Int -> IO a 71 | processFailedException fun cmd args exit_code = 72 | ioError (mkIOError OtherError (fun ++ ": " ++ cmd ++ 73 | concatMap ((' ':) . show) args ++ 74 | " (exit " ++ show exit_code ++ ")") 75 | Nothing Nothing) 76 | 77 | callProcessCwd :: FilePath -> [String] -> FilePath -> IO () 78 | callProcessCwd cmd args cwd = do 79 | exit_code <- withCreateProcess (proc cmd args) { cwd = Just cwd, delegate_ctlc = True } $ \_ _ _ p -> 80 | waitForProcess p 81 | case exit_code of 82 | ExitSuccess -> pure () 83 | ExitFailure r -> processFailedException "callProcess" cmd args r 84 | 85 | compile :: App m => ProjectType -> FilePath -> FilePath -> m (Either String BuildOutput) 86 | compile CombinedJSON _root _src = internalError "unsupported compile type: CombinedJSON" 87 | compile _ root src = do 88 | liftIO $ createDirectory (root "src") 89 | liftIO $ writeFile (root "src" "unit-tests.t.sol") =<< readFile =<< Paths.getDataFileName src 90 | liftIO $ initLib (root "lib" "tokens") ("test" "contracts" "lib" "erc20.sol") "erc20.sol" 91 | liftIO $ initStdForgeDir (root "lib" "forge-std") 92 | (res,out,err) <- liftIO $ readProcessWithExitCode "forge" ["build", "--ast", "--root", root] "" 93 | case res of 94 | ExitFailure _ -> pure . Left $ "compilation failed: " <> "exit code: " <> show res <> "\n\nstdout:\n" <> out <> "\n\nstderr:\n" <> err 95 | ExitSuccess -> readFilteredBuildOutput root (\path -> "unit-tests.t.sol" `Data.List.isInfixOf` path) Foundry 96 | where 97 | initStdForgeDir :: FilePath -> IO () 98 | initStdForgeDir tld = do 99 | createDirectoryIfMissing True tld 100 | forgeStdRepo <- liftIO $ fromMaybe (internalError "cannot find forge-std repo") <$> (lookupEnv "HEVM_FORGE_STD_REPO") 101 | callProcess "mkdir" ["-p", tld] 102 | callProcess "cp" ["-r", forgeStdRepo "src", tld "src"] 103 | initLib :: FilePath -> FilePath -> FilePath -> IO () 104 | initLib tld srcFile dstFile = do 105 | createDirectoryIfMissing True (tld "src") 106 | writeFile (tld "src" dstFile) =<< readFile =<< Paths.getDataFileName srcFile 107 | pure () 108 | -------------------------------------------------------------------------------- /test/clitest.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE QuasiQuotes #-} 2 | 3 | module Main where 4 | 5 | {-| 6 | Description : CLI tests 7 | 8 | This module contains simple CLI test cases to make sure we don't accidentally 9 | break the hevm CLI interface. 10 | 11 | -} 12 | 13 | import Test.Hspec 14 | import System.Process (readProcess, readProcessWithExitCode) 15 | import System.FilePath (()) 16 | import System.Exit (ExitCode(..)) 17 | import Data.List.Split (splitOn) 18 | import Data.Text qualified as T 19 | import Data.String.Here 20 | 21 | import EVM.Solidity 22 | import EVM.Effects 23 | import EVM.Types 24 | 25 | 26 | main :: IO () 27 | main = do 28 | hspec $ 29 | describe "CLI" $ do 30 | it "hevm help works" $ do 31 | (exitCode, stdout, stderr) <- readProcessWithExitCode "cabal" ["run", "exe:hevm", "--", "--help"] "" 32 | stdout `shouldContain` "Usage: hevm" 33 | stdout `shouldContain` "test" 34 | stdout `shouldContain` "equivalence" 35 | stdout `shouldContain` "symbolic" 36 | stdout `shouldContain` "exec" 37 | stdout `shouldContain` "version" 38 | 39 | it "hevm symbolic tutorial works -- FAIL" $ do 40 | Just symbBin <- runApp $ solcRuntime (T.pack "MyContract") (T.pack [i| 41 | contract MyContract { 42 | function simple_symb() public pure { 43 | uint i; 44 | i = 1; 45 | assert(i == 2); 46 | } 47 | } 48 | |]) 49 | let symbHex = bsToHex symbBin 50 | (exitCode, stdout, stderr) <- readProcessWithExitCode "cabal" ["run", "exe:hevm", "--", "symbolic", "--code", symbHex] "" 51 | stdout `shouldContain` "Discovered the following" 52 | exitCode `shouldBe` ExitFailure 1 53 | 54 | (exitCode, stdout, stderr) <- readProcessWithExitCode "cabal" ["run", "exe:hevm", "--", "symbolic", "--code", symbHex, "--sig", "nonexistent()"] "" 55 | stdout `shouldContain` "QED" 56 | exitCode `shouldBe` ExitSuccess 57 | 58 | it "hevm equivalence tutorial works -- FAIL" $ do 59 | let torun = splitOn " " "equivalence --code-a 60003560000260005260016000f3 --code-b 7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60005260016000f3" 60 | (exitCode, stdout, stderr) <- readProcessWithExitCode "cabal" (["run", "exe:hevm", "--" ] ++ torun) "" 61 | stdout `shouldContain` "Not equivalent" 62 | stdout `shouldContain` "FAIL" 63 | exitCode `shouldBe` ExitFailure 1 64 | 65 | it "hevm equivalence tutorial works -- PASS" $ do 66 | Just a <- runApp $ solcRuntime (T.pack "MyContract") (T.pack [i| 67 | contract MyContract { 68 | mapping (address => uint) balances; 69 | function my_adder(address recv, uint amt) public { 70 | if (balances[recv] + amt >= 100) { revert(); } 71 | balances[recv] += amt; 72 | } 73 | } 74 | |]) 75 | let aHex = bsToHex a 76 | Just b <- runApp $ solcRuntime (T.pack "MyContract") (T.pack [i| 77 | contract MyContract { 78 | mapping (address => uint) balances; 79 | function my_adder(address recv, uint amt) public { 80 | if (balances[recv] + amt >= 100) { revert(); } 81 | balances[recv] += amt/2; 82 | balances[recv] += amt/2; 83 | if (amt % 2 != 0) balances[recv]++; 84 | } 85 | } 86 | |]) 87 | let bHex = bsToHex b 88 | (exitCode, stdout, stderr) <- readProcessWithExitCode "cabal" (["run", "exe:hevm", "--", "equivalence" , "--code-a", aHex, "--code-b", bHex ]) "" 89 | stdout `shouldContain` "No discrepancies found" 90 | stdout `shouldContain` "PASS" 91 | exitCode `shouldBe` ExitSuccess 92 | 93 | it "hevm concrete tutorial works" $ do 94 | let torun = splitOn " " "exec --code 0x647175696e6550383480393834f3 --gas 0xff" 95 | (exitCode, stdout, stderr) <- readProcessWithExitCode "cabal" (["run", "exe:hevm", "--" ] ++ torun) "" 96 | stdout `shouldContain` "Return: 0x64" 97 | exitCode `shouldBe` ExitSuccess 98 | 99 | it "warning on zero address" $ do 100 | Just c <- runApp $ solcRuntime (T.pack "C") (T.pack [i| 101 | contract Target { 102 | function get() external view returns (uint256) { 103 | return 55; 104 | } 105 | } 106 | contract C { 107 | Target mm; 108 | function retFor() public returns (uint256) { 109 | Target target = Target(address(0)); 110 | uint256 ret = target.get(); 111 | assert(ret == 4); 112 | return ret; 113 | } 114 | } 115 | |]) 116 | let hexStr = bsToHex c 117 | (exitCode, stdout, stderr) <- readProcessWithExitCode "cabal" ["run", "exe:hevm", "--", "symbolic", "--code", hexStr] "" 118 | stdout `shouldContain` "Warning: fetching contract at address 0" 119 | 120 | -- file "devcon_example.yul" from "eq-all-yul-optimization-tests" in test.hs 121 | -- we check that at least one UNSAT cache hit happens, i.e. the unsat cache is not 122 | -- completely broken 123 | it "unsat-cache" $ do 124 | let a = [i| { 125 | calldatacopy(0,0,1024) 126 | sstore(0, array_sum(calldataload(0))) 127 | function array_sum(x) -> sum { 128 | let length := calldataload(x) 129 | for { let i := 0 } lt(i, length) { i := add(i, 1) } { 130 | sum := add(sum, array_load(x, i)) 131 | } 132 | } 133 | function array_load(x, i) -> v { 134 | let len := calldataload(x) 135 | if iszero(lt(i, len)) { revert(0, 0) } 136 | let data := add(x, 0x20) 137 | v := calldataload(add(data, mul(i, 0x20))) 138 | } 139 | } |] 140 | let b = [i| { 141 | calldatacopy(0,0,1024) 142 | { 143 | let _1 := calldataload(0) 144 | let sum := 0 145 | let length := calldataload(_1) 146 | let i := 0 147 | for { } true { i := add(i, 1) } 148 | { 149 | let _2 := iszero(lt(i, length)) 150 | if _2 { break } 151 | _2 := 0 152 | sum := add(sum, calldataload(add(add(_1, shl(5, i)), 0x20))) 153 | } 154 | sstore(0, sum) 155 | } 156 | } |] 157 | Just aPrgm <- yul (T.pack "") $ T.pack a 158 | Just bPrgm <- yul (T.pack "") $ T.pack b 159 | let hexStrA = bsToHex aPrgm 160 | hexStrB = bsToHex bPrgm 161 | (exitCode, stdout, stderr) <- readProcessWithExitCode "cabal" ["run", "exe:hevm", "--", "equivalence", "--num-solvers", "1", "--debug", "--code-a", hexStrA, "--code-b", hexStrB] "" 162 | stdout `shouldContain` "Qed found via cache" 163 | 164 | -------------------------------------------------------------------------------- /test/contracts/fail/10_BadVault.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | // contributed by @karmacoma on 5 Aug 2023 3 | 4 | pragma solidity ^0.8.15; 5 | 6 | import "forge-std/Test.sol"; 7 | import {console2} from "forge-std/console2.sol"; 8 | 9 | // inspired by the classic reentrancy level in Ethernaut CTF 10 | contract BadVault { 11 | mapping(address => uint256) public balance; 12 | 13 | function deposit() external payable { 14 | balance[msg.sender] += msg.value; 15 | 16 | // console2.log("deposit", msg.sender, msg.value); 17 | } 18 | 19 | function withdraw(uint256 amount) external { 20 | // checks 21 | uint256 _balance = balance[msg.sender]; 22 | require(_balance >= amount, "insufficient balance"); 23 | 24 | // console2.log("withdraw", msg.sender, amount); 25 | 26 | // interactions 27 | (bool success,) = msg.sender.call{value: amount}(""); 28 | require(success, "transfer failed"); 29 | 30 | // effects 31 | balance[msg.sender] = _balance - amount; 32 | } 33 | } 34 | 35 | // from https://github.com/mds1/multicall 36 | struct Call3Value { 37 | address target; 38 | uint256 value; 39 | bytes32 sig; 40 | bytes32 data; 41 | } 42 | 43 | contract ExploitLaunchPad { 44 | address public owner; 45 | bool reentered; 46 | 47 | Call3Value public call; 48 | 49 | constructor() { 50 | owner = msg.sender; 51 | } 52 | 53 | receive() external payable { 54 | if (reentered) { 55 | return; 56 | } 57 | 58 | require(call.value <= address(this).balance, "insufficient balance"); 59 | 60 | reentered = true; 61 | (bool success,) = call.target.call{value: call.value}(abi.encodePacked(call.sig, call.data)); 62 | reentered = false; 63 | } 64 | 65 | function defer(Call3Value calldata _call) external payable { 66 | require(msg.sender == owner, "only owner"); 67 | call = _call; 68 | } 69 | 70 | function go(Call3Value calldata _call) 71 | external 72 | payable 73 | { 74 | require(msg.sender == owner, "only owner"); 75 | require(_call.value <= address(this).balance, "insufficient balance"); 76 | 77 | (bool success,) = _call.target.call{value: _call.value}(abi.encodePacked(_call.sig, _call.data)); 78 | } 79 | 80 | function deposit() external payable {} 81 | 82 | function withdraw() external { 83 | owner.call{value: address(this).balance}(""); 84 | } 85 | } 86 | 87 | contract BadVaultTest is Test { 88 | BadVault vault; 89 | ExploitLaunchPad exploit; 90 | 91 | address user1; 92 | address user2; 93 | address attacker; 94 | 95 | function setUp() public { 96 | vault = new BadVault(); 97 | 98 | user1 = address(1); 99 | user2 = address(2); 100 | 101 | vm.deal(user1, 1 ether); 102 | vm.prank(user1); 103 | vault.deposit{value: 1 ether}(); 104 | 105 | vm.deal(user2, 1 ether); 106 | vm.prank(user2); 107 | vault.deposit{value: 1 ether}(); 108 | 109 | attacker = address(42); 110 | vm.prank(attacker); 111 | exploit = new ExploitLaunchPad(); 112 | 113 | assert(exploit.owner() == attacker); 114 | console2.log("HI"); 115 | console2.log(exploit.owner()); 116 | console2.log(attacker); 117 | } 118 | 119 | /// @custom:halmos --array-lengths data1=36,data2=36,deferredData=36 120 | function prove_BadVault_usingExploitLaunchPad( 121 | address target1, 122 | uint256 amount1, 123 | bytes32 sig1, 124 | bytes32 data1, 125 | 126 | address target2, 127 | uint256 amount2, 128 | bytes32 sig2, 129 | bytes32 data2, 130 | 131 | address deferredTarget, 132 | uint256 deferredAmount, 133 | bytes32 deferredSig, 134 | bytes32 deferredData 135 | 136 | ) public { 137 | uint256 STARTING_BALANCE = 2 ether; 138 | vm.deal(attacker, STARTING_BALANCE); 139 | 140 | vm.assume(address(exploit).balance == 0); 141 | vm.assume((amount1 + amount2) <= STARTING_BALANCE); 142 | 143 | // console2.log("attacker starting balance", address(attacker).balance); 144 | vm.prank(attacker); 145 | exploit.deposit{value: STARTING_BALANCE}(); 146 | 147 | vm.prank(attacker); 148 | exploit.go(Call3Value({ 149 | target: address(vault), 150 | value: amount1, 151 | sig: sig1, 152 | data: data1 153 | })); 154 | 155 | vm.prank(attacker); 156 | exploit.defer(Call3Value({ 157 | target: address(vault), 158 | value: deferredAmount, 159 | sig: deferredSig, 160 | data: deferredData 161 | })); 162 | 163 | vm.prank(attacker); 164 | exploit.go(Call3Value({ 165 | target: address(vault), 166 | value: amount2, 167 | sig: sig2, 168 | data: data2 169 | })); 170 | 171 | vm.prank(attacker); 172 | exploit.withdraw(); 173 | 174 | // they can not end up with more ether than they started with 175 | // console2.log("attacker final balance", address(attacker).balance); 176 | assert(attacker.balance <= STARTING_BALANCE); 177 | } 178 | 179 | // running `forge test --match-test test_BadVault_solution -vvv` confirms the attack trace: 180 | //took 6s Node system at 18:00:43 181 | // deposit 0x0000000000000000000000000000000000000001 1000000000000000000 182 | // deposit 0x0000000000000000000000000000000000000002 1000000000000000000 183 | // attacker starting balance 2000000000000000000 184 | // deposit 0x5f4E4CcFF0A2553b2BDE30e1fC8531B287db9087 1000000000000000000 185 | // withdraw 0x5f4E4CcFF0A2553b2BDE30e1fC8531B287db9087 1000000000000000000 186 | // withdraw 0x5f4E4CcFF0A2553b2BDE30e1fC8531B287db9087 1000000000000000000 187 | // attacker final balance 3000000000000000000 188 | // function test_BadVault_solution() public { 189 | // prove_BadVault_usingExploitLaunchPad( 190 | // // 1st call 191 | // address(vault), 192 | // 1 ether, 193 | // abi.encodeWithSelector( 194 | // vault.deposit.selector 195 | // ), 196 | 197 | // // 2nd call 198 | // address(vault), 199 | // 0 ether, 200 | // abi.encodeWithSelector( 201 | // vault.withdraw.selector, 202 | // 1 ether 203 | // ), 204 | 205 | // // deferred call 206 | // address(vault), 207 | // 0 ether, 208 | // abi.encodeWithSelector( 209 | // vault.withdraw.selector, 210 | // 1 ether 211 | // ) 212 | // ); 213 | // } 214 | } 215 | -------------------------------------------------------------------------------- /test/contracts/fail/check-prefix.sol: -------------------------------------------------------------------------------- 1 | import "forge-std/Test.sol"; 2 | 3 | contract SolidityTest is Test { 4 | 5 | function setUp() public { 6 | } 7 | 8 | function prove_trivial() public { 9 | assertTrue(false); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/contracts/fail/dsProveFail.sol: -------------------------------------------------------------------------------- 1 | import "forge-std/Test.sol"; 2 | import "tokens/erc20.sol"; 3 | 4 | contract SolidityTest is Test { 5 | ERC20 token; 6 | 7 | function setUp() public { 8 | token = new ERC20("TOKEN", "TKN", 18); 9 | } 10 | 11 | function prove_trivial() public { 12 | assert(false); 13 | } 14 | 15 | function prove_all_branches_fail() public { 16 | require(false); 17 | } 18 | 19 | function prove_trivial_dstest() public { 20 | assertEq(uint(1), uint(2)); 21 | } 22 | 23 | function prove_add(uint x, uint y) public { 24 | unchecked { 25 | assertTrue(x + y >= x); 26 | } 27 | } 28 | 29 | function prove_smtTimeout(uint x, uint y, uint z) public { 30 | if ((x * y / z) * (x / y) / (x * y) == (x * x * x * y * z / x * z * y)) { 31 | assertTrue(false); 32 | } else { 33 | assertTrue(true); 34 | } 35 | } 36 | 37 | function prove_multi(uint x) public { 38 | if (x == 3) { 39 | assertTrue(false); 40 | } else if (x == 9) { 41 | assertTrue(false); 42 | } else if (x == 1023423194871904872390487213) { 43 | assertTrue(false); 44 | } else { 45 | assertTrue(true); 46 | } 47 | } 48 | 49 | function prove_distributivity(uint120 x, uint120 y, uint120 z) public { 50 | assertEq(x + (y * z), (x + y) * (x + z)); 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /test/contracts/fail/trivial.sol: -------------------------------------------------------------------------------- 1 | import {Test} from "forge-std/Test.sol"; 2 | 3 | // should run and pass 4 | contract Trivial is Test { 5 | function prove_false() public { 6 | assertTrue(false); 7 | } 8 | } 9 | 10 | -------------------------------------------------------------------------------- /test/contracts/lib/erc20.sol: -------------------------------------------------------------------------------- 1 | // modified from solmate erc20 2 | // https://github.com/transmissions11/solmate/blob/c2594bf4635ad773a8f4763e20b7e79582e41535/src/tokens/ERC20.sol 3 | 4 | /// @notice Modern and gas efficient ERC20 + EIP-2612 implementation. 5 | /// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC20.sol) 6 | /// @author Modified from Uniswap (https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol) 7 | /// @dev Do not manually set balances without updating totalSupply, as the sum of all user balances must not exceed it. 8 | contract ERC20 { 9 | 10 | // --- events --- 11 | 12 | event Transfer(address indexed from, address indexed to, uint256 amount); 13 | event Approval(address indexed owner, address indexed spender, uint256 amount); 14 | 15 | // --- metadata --- 16 | 17 | string public name; 18 | string public symbol; 19 | uint8 public immutable decimals; 20 | 21 | // --- erc20 data --- 22 | 23 | uint256 public totalSupply; 24 | mapping(address => uint256) public balanceOf; 25 | mapping(address => mapping(address => uint256)) public allowance; 26 | 27 | // --- eip2612 data --- 28 | 29 | uint256 internal immutable INITIAL_CHAIN_ID; 30 | bytes32 internal immutable INITIAL_DOMAIN_SEPARATOR; 31 | mapping(address => uint256) public nonces; 32 | 33 | // --- admin --- 34 | 35 | address public owner; 36 | modifier auth { require(owner == msg.sender, "not-authorized"); _; } 37 | 38 | // --- init --- 39 | 40 | constructor( 41 | string memory _name, 42 | string memory _symbol, 43 | uint8 _decimals 44 | ) { 45 | name = _name; 46 | symbol = _symbol; 47 | decimals = _decimals; 48 | 49 | INITIAL_CHAIN_ID = block.chainid; 50 | INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator(); 51 | 52 | owner = msg.sender; 53 | } 54 | 55 | // --- erc20 logic --- 56 | 57 | function approve(address spender, uint256 amount) public returns (bool) { 58 | allowance[msg.sender][spender] = amount; 59 | 60 | emit Approval(msg.sender, spender, amount); 61 | 62 | return true; 63 | } 64 | 65 | function transfer(address to, uint256 amount) public returns (bool) { 66 | balanceOf[msg.sender] -= amount; 67 | 68 | // Cannot overflow because the sum of all user 69 | // balances can't exceed the max uint256 value. 70 | unchecked { 71 | balanceOf[to] += amount; 72 | } 73 | 74 | emit Transfer(msg.sender, to, amount); 75 | 76 | return true; 77 | } 78 | 79 | function transferFrom( 80 | address from, 81 | address to, 82 | uint256 amount 83 | ) public returns (bool) { 84 | uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals. 85 | 86 | if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount; 87 | 88 | balanceOf[from] -= amount; 89 | 90 | // Cannot overflow because the sum of all user 91 | // balances can't exceed the max uint256 value. 92 | unchecked { 93 | balanceOf[to] += amount; 94 | } 95 | 96 | emit Transfer(from, to, amount); 97 | 98 | return true; 99 | } 100 | 101 | // --- mint / burn logic --- 102 | 103 | function mint(address to, uint256 amount) public auth { 104 | _mint(to, amount); 105 | } 106 | 107 | function burn(address from, uint256 amount) public auth { 108 | _burn(from, amount); 109 | } 110 | 111 | // --- eip2612 logic --- 112 | 113 | function permit( 114 | address owner, 115 | address spender, 116 | uint256 value, 117 | uint256 deadline, 118 | uint8 v, 119 | bytes32 r, 120 | bytes32 s 121 | ) public { 122 | require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED"); 123 | 124 | // Unchecked because the only math done is incrementing 125 | // the owner's nonce which cannot realistically overflow. 126 | unchecked { 127 | address recoveredAddress = ecrecover( 128 | keccak256( 129 | abi.encodePacked( 130 | "\x19\x01", 131 | DOMAIN_SEPARATOR(), 132 | keccak256( 133 | abi.encode( 134 | keccak256( 135 | "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" 136 | ), 137 | owner, 138 | spender, 139 | value, 140 | nonces[owner]++, 141 | deadline 142 | ) 143 | ) 144 | ) 145 | ), 146 | v, 147 | r, 148 | s 149 | ); 150 | 151 | require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNER"); 152 | 153 | allowance[recoveredAddress][spender] = value; 154 | } 155 | 156 | emit Approval(owner, spender, value); 157 | } 158 | 159 | function DOMAIN_SEPARATOR() public view returns (bytes32) { 160 | return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : computeDomainSeparator(); 161 | } 162 | 163 | function computeDomainSeparator() internal view returns (bytes32) { 164 | return 165 | keccak256( 166 | abi.encode( 167 | keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), 168 | keccak256(bytes(name)), 169 | keccak256("1"), 170 | block.chainid, 171 | address(this) 172 | ) 173 | ); 174 | } 175 | 176 | // --- internal mint / burn logic --- 177 | 178 | function _mint(address to, uint256 amount) internal { 179 | totalSupply += amount; 180 | 181 | // Cannot overflow because the sum of all user 182 | // balances can't exceed the max uint256 value. 183 | unchecked { 184 | balanceOf[to] += amount; 185 | } 186 | 187 | emit Transfer(address(0), to, amount); 188 | } 189 | 190 | function _burn(address from, uint256 amount) internal { 191 | balanceOf[from] -= amount; 192 | 193 | // Cannot underflow because a user's balance 194 | // will never be larger than the total supply. 195 | unchecked { 196 | totalSupply -= amount; 197 | } 198 | 199 | emit Transfer(from, address(0), amount); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /test/contracts/pass/abstract.sol: -------------------------------------------------------------------------------- 1 | import {Test} from "forge-std/Test.sol"; 2 | 3 | // should not be run (no code) 4 | abstract contract MyTest is Test { 5 | function testAbstract() public { 6 | assertTrue(true); 7 | } 8 | } 9 | 10 | // should run and pass 11 | contract TestMy is MyTest { 12 | function testTrue() public { 13 | assertTrue(true); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/contracts/pass/cheatCodesFork.sol: -------------------------------------------------------------------------------- 1 | pragma experimental ABIEncoderV2; 2 | 3 | import "forge-std/Test.sol"; 4 | 5 | interface Hevm { 6 | function warp(uint256) external; 7 | function roll(uint256) external; 8 | function load(address,bytes32) external returns (bytes32); 9 | function store(address,bytes32,bytes32) external; 10 | function sign(uint256,bytes32) external returns (uint8,bytes32,bytes32); 11 | function addr(uint256) external returns (address); 12 | function ffi(string[] calldata) external returns (bytes memory); 13 | function prank(address) external; 14 | function startPrank(address) external; 15 | function stopPrank() external; 16 | function deal(address,uint256) external; 17 | function createFork(string calldata urlOrAlias) external returns (uint256); 18 | function selectFork(uint256 forkId) external; 19 | function activeFork() external returns (uint256); 20 | } 21 | 22 | /// @dev This contract's state should depend on which fork we are on 23 | contract TestState { 24 | uint256 public state; 25 | function setState(uint256 _state) external { 26 | state = _state; 27 | } 28 | } 29 | 30 | /// @dev This contract's state should be persistent across forks, because it's the contract calling `selectFork` 31 | contract CheatCodesForkDeployee is Test { 32 | address constant HEVM_ADDRESS = 33 | address(bytes20(uint160(uint256(keccak256('hevm cheat code'))))); 34 | Hevm hevm = Hevm(HEVM_ADDRESS); 35 | address stateContract; 36 | uint256 forkId1; 37 | uint256 forkId2; 38 | uint256 persistentState; 39 | 40 | constructor() { 41 | stateContract = address(new TestState()); 42 | forkId1 = hevm.createFork("foo"); // If the default fork's id is 0, then this would be 1 43 | forkId2 = hevm.createFork("bar"); // and this would be 2 44 | persistentState = 0; 45 | } 46 | 47 | function deployee_prove_ForkedState() external { 48 | hevm.selectFork(0); // Default fork 49 | assert(hevm.activeFork() == 0); // Check active fork 50 | persistentState = 1; // Make sure this contract maintains its own state across fork 51 | hevm.selectFork(forkId1); // Fork 1 52 | assert(hevm.activeFork() == forkId1); // Check active fork 53 | assert(TestState(stateContract).state() == 0); // Check initial external state 54 | assert(persistentState == 1); // Check persistent state 55 | persistentState = 2; // Set persistent state 56 | TestState(stateContract).setState(1); // Set unique external state 57 | hevm.roll(12345678); // Set unique block number 58 | hevm.selectFork(forkId2); // Fork 2 59 | assert(hevm.activeFork() == forkId2); // Check active fork 60 | assert(TestState(stateContract).state() == 0); // Check initial external state 61 | assert(persistentState == 2); // Check persistent state 62 | persistentState = 3; // Set persistent state 63 | TestState(stateContract).setState(2); // Set unique external state 64 | hevm.roll(23456789); // Set unique block number 65 | hevm.selectFork(forkId1); // Fork 1 66 | assert(hevm.activeFork() == forkId1); // Check active fork 67 | assert(block.number == 12345678); // Check unique block number 68 | assert(TestState(stateContract).state() == 1); // Check unique external state 69 | assert(persistentState == 3); // Check persistent state 70 | persistentState = 4; // Set persistent state 71 | TestState(stateContract).setState(0); // Set initial external state 72 | hevm.selectFork(forkId2); // Fork 2 73 | assert(hevm.activeFork() == forkId2); // Check active fork 74 | assert(block.number == 23456789); // Check unique block number 75 | assert(TestState(stateContract).state() == 2); // Check unique external state 76 | assert(persistentState == 4); // Check persistent state 77 | persistentState = 5; // Set persistent state 78 | TestState(stateContract).setState(0); // Set initial external state 79 | hevm.selectFork(forkId1); // Fork 1 80 | assert(hevm.activeFork() == forkId1); // Check active fork 81 | hevm.deal(address(this), 10); // Get some eth 82 | payable(msg.sender).send(10); // Send eth to msg.sender 83 | uint256 senderBalance = msg.sender.balance; // Record msg.sender's balance 84 | hevm.selectFork(forkId2); // Fork 2 85 | assert(hevm.activeFork() == forkId2); // Check active fork 86 | assert(msg.sender.balance == senderBalance); // Check msg.sender's balance 87 | hevm.selectFork(0); // Default fork 88 | assert(hevm.activeFork() == 0); // Check active fork 89 | assert(persistentState == 5); // Check persistent state 90 | } 91 | } 92 | 93 | /// @dev This contract's state should be persistent across forks, because it's the `msg.sender` when running `deployee_prove_ForkedState`. 94 | /// We need this "deployer/deployee" architecture so that `msg.sender` will be concrete when running `deployee_prove_ForkedState`. 95 | /// If we were to only use the `CheatCodesForkDeployee` contract, the `msg.sender` would be abstract. 96 | contract CheatCodesFork is Test { 97 | CheatCodesForkDeployee testContract = new CheatCodesForkDeployee(); 98 | function prove_ForkedState() external { 99 | testContract.deployee_prove_ForkedState(); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /test/contracts/pass/constantinople_min.sol: -------------------------------------------------------------------------------- 1 | import "forge-std/Test.sol"; 2 | 3 | contract DeadCode{ 4 | function dummy() external returns (uint256) {} 5 | } 6 | 7 | contract ConstantinopleTests is Test { 8 | DeadCode notmuch; 9 | function setUp() public { 10 | notmuch = new DeadCode(); 11 | } 12 | 13 | // this 5 byte-long initcode simply returns nothing 14 | // PUSH1 00 PUSH1 00 RETURN 15 | // 60 00 60 00 f3 16 | bytes32 zerocode = 0x60006000f3000000000000000000000000000000000000000000000000000000; 17 | // this 13 byte-long initcode simply returns 0xdeadbeef: 18 | // PUSH4 de ad be ef PUSH1 00 MSTORE PUSH1 32 PUSH1 00 RETURN 19 | // 63 de ad be ef 60 00 52 60 20 60 00 f3 20 | bytes32 deadcode = 0x63deadbeef60005260206000f300000000000000000000000000000000000000; 21 | // this 25 byte-long initcode returns deadcode (but without the padding) 22 | // PUSH1 0d PUSH1 0c PUSH1 00 CODECO PUSH1 0d PUSH1 00 RETURN deadcode 23 | // 60 0d 60 0c 60 00 39 60 0d 60 00 f3 24 | bytes32 deploysdeadcode = 0x600d600c600039600d6000f363deadbeef60005260206000f300000000000000; 25 | 26 | // address of account created by CREATE2 is 27 | // keccak256(0xff + address + salt + keccak256(init_code))[12:] 28 | function check_create2_1() public { 29 | address a; 30 | uint256 salt = 0xfacefeed; 31 | assembly { 32 | let top := mload(0x40) 33 | mstore(top, sload(deadcode.slot)) 34 | a := create2(0, top, 13, salt) 35 | } 36 | 37 | address expected_a; 38 | 39 | assembly { 40 | let top := mload(0x40) 41 | mstore(top, sload(deadcode.slot)) 42 | let inithash := keccak256(top, 13) 43 | mstore(sub(top, 11), address()) 44 | mstore8(top, 0xff) 45 | mstore(add(top, 21), salt) 46 | mstore(add(top, 53), inithash) 47 | expected_a := and(keccak256(top, 85), 0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff) 48 | } 49 | 50 | assertEq(a, expected_a); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/contracts/pass/dsProvePass.sol: -------------------------------------------------------------------------------- 1 | import "forge-std/Test.sol"; 2 | import "tokens/erc20.sol"; 3 | 4 | contract ConstructorArg { 5 | address immutable public a; 6 | constructor(address _a) public { 7 | a = _a; 8 | } 9 | } 10 | 11 | contract SolidityTest is Test { 12 | ERC20 token; 13 | 14 | function setUp() public { 15 | token = new ERC20("Token", "TKN", 18); 16 | } 17 | 18 | function prove_trivial() public { 19 | assertTrue(true); 20 | } 21 | 22 | function prove_easy(uint v) public { 23 | if (v != 100) return; 24 | assertEq(v, 100); 25 | } 26 | 27 | function prove_add(uint x, uint y) public { 28 | unchecked { 29 | if (x + y < x) return; // no overflow 30 | assertTrue(x + y >= x); 31 | } 32 | } 33 | 34 | function prove_balance(address usr, uint amt) public { 35 | assertEq(0, token.balanceOf(usr)); 36 | token.mint(usr, amt); 37 | assertEq(amt, token.balanceOf(usr)); 38 | } 39 | 40 | function prove_supply(uint supply) public { 41 | token.mint(address(this), supply); 42 | uint actual = token.totalSupply(); 43 | assertEq(supply, actual); 44 | } 45 | 46 | function prove_constructorArgs(address b) public { 47 | ConstructorArg c = new ConstructorArg(b); 48 | assertEq(b, c.a()); 49 | } 50 | 51 | // requires do not trigger a failure in `prove_` tests 52 | function prove_no_fail_require(uint x) public { 53 | if (x == 2) { 54 | require(false); 55 | } else { 56 | assert(x != 2); 57 | } 58 | } 59 | 60 | // all branches in a proveFail test must end in one of: 61 | // - a require 62 | // - a failed user defined assert 63 | // - a failed ds-test assertion violation 64 | function proveFail_require(uint x) public { 65 | require(x == 100); 66 | assert(false); 67 | } 68 | 69 | function proveFail_userAssertSmoke() public { 70 | assert(false); 71 | } 72 | 73 | function proveFail_assertSmoke() public { 74 | assertTrue(false); 75 | } 76 | 77 | function prove_burn(uint supply, uint amt) public { 78 | if (amt > supply) return; // no underflow 79 | 80 | token.mint(address(this), supply); 81 | token.burn(address(this), amt); 82 | 83 | assertEq(supply - amt, token.totalSupply()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /test/contracts/pass/libraries.sol: -------------------------------------------------------------------------------- 1 | import "ds-test/test.sol"; 2 | 3 | library A { 4 | function f(uint128 x) public returns (uint256) { 5 | return uint(x) * 2; 6 | } 7 | } 8 | 9 | contract B is DSTest { 10 | using A for uint128; 11 | function test_f(uint128 x) public { 12 | assertEq(uint(x) * 2, x.f()); 13 | } 14 | 15 | function testFail_f() public { 16 | assertEq(1, uint128(1).f()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/contracts/pass/loops.sol: -------------------------------------------------------------------------------- 1 | import "forge-std/Test.sol"; 2 | 3 | contract Loops is Test { 4 | 5 | function prove_loop(uint n) public { 6 | uint counter = 0; 7 | for (uint i = 0; i < n; i++) { 8 | counter++; 9 | } 10 | assertTrue(counter < 100); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/contracts/pass/no-overapprox-delegatecall.sol: -------------------------------------------------------------------------------- 1 | 2 | // This test overapproximates the delegatecall 3 | // if an SMT solver is not called to check that there is only one solution to the address masking 4 | import {Test} from "forge-std/Test.sol"; 5 | 6 | contract ERC20 { 7 | address any; 8 | function f() public { 9 | any = msg.sender; 10 | } 11 | } 12 | 13 | contract TEST is Test{ 14 | address[] tokens; 15 | address any = address(0x1234); 16 | mapping(address => uint256) balances; 17 | 18 | constructor() { 19 | tokens.push(address(new ERC20())); 20 | } 21 | 22 | function prove_no_overapprox(address target) public { 23 | balances[target] = any.balance; 24 | address(tokens[0]).delegatecall(abi.encodeWithSignature("f()")); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/contracts/pass/no-overapprox-staticcall.sol: -------------------------------------------------------------------------------- 1 | 2 | // This test overapproximates the staticcall (it's a "view" function, so it's a staticcall) 3 | // if an SMT solver is not called to check that there is only one solution to the address masking 4 | import {Test} from "forge-std/Test.sol"; 5 | 6 | contract ERC20 { 7 | function f() public view { } 8 | } 9 | 10 | contract TEST is Test{ 11 | address[] tokens; 12 | address any = address(0x1234); 13 | mapping(address => uint256) balances; 14 | 15 | function setUp() public { 16 | tokens.push(address(new ERC20())); 17 | } 18 | 19 | function prove_no_overapprox(address target) public { 20 | balances[target] = any.balance; 21 | ERC20(address(tokens[0])).f(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/contracts/pass/rpc.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.19; 2 | 3 | import {Test} from "forge-std/Test.sol"; 4 | import {ERC20} from "tokens/erc20.sol"; 5 | 6 | contract C is Test { 7 | // BAL: https://etherscan.io/address/0xba100000625a3754423978a60c9317c58a424e3D#code 8 | ERC20 bal = ERC20(0xba100000625a3754423978a60c9317c58a424e3D); 9 | 10 | function prove_trivial() public { 11 | uint ub = bal.balanceOf(0xBA12222222228d8Ba445958a75a0704d566BF2C8); 12 | assertEq(ub, 27099516537379438397130892); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/contracts/pass/transfer.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.19; 2 | 3 | import "forge-std/Test.sol"; 4 | import {ERC20} from "tokens/erc20.sol"; 5 | 6 | contract SolidityTestFail2 is Test { 7 | ERC20 token; 8 | 9 | function setUp() public { 10 | token = new ERC20("TOKEN", "TKN", 18); 11 | } 12 | 13 | function prove_transfer(uint supply, address usr, uint amt) public { 14 | // require(supply >= amt, "supply must be greater than or equal to amt"); 15 | token.mint(address(this), supply); 16 | 17 | uint prebal = token.balanceOf(usr); 18 | token.transfer(usr, amt); 19 | uint postbal = token.balanceOf(usr); 20 | 21 | uint expected = usr == address(this) 22 | ? 0 // self transfer is a noop 23 | : amt; // otherwise `amt` has been transferred to `usr` 24 | assertTrue(expected == postbal - prebal); 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /test/contracts/pass/trivial.sol: -------------------------------------------------------------------------------- 1 | import {Test} from "forge-std/Test.sol"; 2 | 3 | // should run and pass 4 | contract Trivial is Test { 5 | function prove_true() public { 6 | assertTrue(true); 7 | } 8 | } 9 | 10 | -------------------------------------------------------------------------------- /test/contracts/pass/unwind.sol: -------------------------------------------------------------------------------- 1 | import {Test} from "forge-std/Test.sol"; 2 | 3 | // tests unwind support in precompiles 4 | contract Unwind is Test { 5 | function prove_invalid_sum() public { 6 | bytes32 x = hex"01"; 7 | try this.callBn256Add(x, x, x, x) returns (bytes32[2] memory) { 8 | failed(); 9 | } catch (bytes memory) { } 10 | } 11 | 12 | function callBn256Add( 13 | bytes32 ax, 14 | bytes32 ay, 15 | bytes32 bx, 16 | bytes32 by 17 | ) external returns (bytes32[2] memory result) { 18 | bytes32[4] memory input; 19 | input[0] = ax; 20 | input[1] = ay; 21 | input[2] = bx; 22 | input[3] = by; 23 | assembly { 24 | let success := call(gas(), 0x06, 0, input, 0x80, result, 0x40) 25 | switch success 26 | case 0 { 27 | revert(0, 0) 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/rpc.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DataKinds #-} 2 | 3 | module Main where 4 | 5 | import Test.Tasty 6 | import Test.Tasty.HUnit 7 | 8 | import Data.Maybe 9 | import Data.Map qualified as Map 10 | import Data.Text (Text) 11 | import Data.Vector qualified as V 12 | 13 | import Optics.Core 14 | import EVM (makeVm, symbolify, forceLit) 15 | import EVM.ABI 16 | import EVM.Fetch 17 | import EVM.SMT 18 | import EVM.Solvers 19 | import EVM.Stepper qualified as Stepper 20 | import EVM.SymExec 21 | import EVM.Test.Utils 22 | import EVM.Solidity (ProjectType(..)) 23 | import EVM.Types hiding (BlockNumber, Env) 24 | import Control.Monad.ST (stToIO, RealWorld) 25 | import Control.Monad.Reader (ReaderT) 26 | import Control.Monad.IO.Unlift 27 | import EVM.Effects 28 | 29 | rpcEnv :: Env 30 | rpcEnv = Env { config = defaultConfig } 31 | 32 | test :: TestName -> ReaderT Env IO () -> TestTree 33 | test a b = testCase a $ runEnv rpcEnv b 34 | 35 | main :: IO () 36 | main = defaultMain tests 37 | 38 | tests :: TestTree 39 | tests = testGroup "rpc" 40 | [ testGroup "Block Parsing Tests" 41 | [ testCase "pre-merge-block" $ do 42 | let block = BlockNumber 15537392 43 | (cb, numb, basefee, prevRan) <- fetchBlockFrom block testRpc >>= \case 44 | Nothing -> internalError "Could not fetch block" 45 | Just Block{..} -> pure ( coinbase 46 | , number 47 | , baseFee 48 | , prevRandao 49 | ) 50 | 51 | assertEqual "coinbase" (LitAddr 0xea674fdde714fd979de3edf0f56aa9716b898ec8) cb 52 | assertEqual "number" (BlockNumber (forceLit numb)) block 53 | assertEqual "basefee" 38572377838 basefee 54 | assertEqual "prevRan" 11049842297455506 prevRan 55 | , testCase "post-merge-block" $ do 56 | let block = BlockNumber 16184420 57 | (cb, numb, basefee, prevRan) <- fetchBlockFrom block testRpc >>= \case 58 | Nothing -> internalError "Could not fetch block" 59 | Just Block{..} -> pure ( coinbase 60 | , number 61 | , baseFee 62 | , prevRandao 63 | ) 64 | 65 | assertEqual "coinbase" (LitAddr 0x690b9a9e9aa1c9db991c7721a92d351db4fac990) cb 66 | assertEqual "number" (BlockNumber (forceLit numb)) block 67 | assertEqual "basefee" 22163046690 basefee 68 | assertEqual "prevRan" 0x2267531ab030ed32fd5f2ef51f81427332d0becbd74fe7f4cd5684ddf4b287e0 prevRan 69 | ] 70 | , testGroup "execution with remote state" 71 | -- execute against remote state from a ds-test harness 72 | [ test "dapp-test" $ do 73 | let testFile = "test/contracts/pass/rpc.sol" 74 | res <- runSolidityTestCustom testFile ".*" Nothing Nothing False testRpcInfo Foundry 75 | liftIO $ assertEqual "test result" (True, True) res 76 | 77 | -- concretely exec "transfer" on WETH9 using remote rpc 78 | -- https://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2#code 79 | , test "weth-conc" $ do 80 | let 81 | blockNum = 16198552 82 | wad = 0x999999999999999999 83 | calldata' = ConcreteBuf $ abiMethod "transfer(address,uint256)" (AbiTuple (V.fromList [AbiAddress (Addr 0xdead), AbiUInt 256 wad])) 84 | vm <- liftIO $ weth9VM blockNum (calldata', []) 85 | postVm <- withSolvers Z3 1 1 Nothing $ \solvers -> 86 | Stepper.interpret (oracle solvers (Just (BlockNumber blockNum, testRpc))) vm Stepper.runFully 87 | let 88 | wethStore = (fromJust $ Map.lookup (LitAddr 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2) postVm.env.contracts).storage 89 | wethStore' = case wethStore of 90 | ConcreteStore s -> s 91 | _ -> internalError "Expecting concrete store" 92 | receiverBal = fromJust $ Map.lookup (keccak' (word256Bytes 0xdead <> word256Bytes 0x3)) wethStore' 93 | msg = case postVm.result of 94 | Just (VMSuccess m) -> m 95 | _ -> internalError "VMSuccess expected" 96 | liftIO $ do 97 | assertEqual "should succeed" msg (ConcreteBuf $ word256Bytes 0x1) 98 | assertEqual "should revert" receiverBal (W256 $ 2595433725034301 + wad) 99 | 100 | -- symbolically exec "transfer" on WETH9 using remote rpc 101 | -- https://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2#code 102 | , test "weth-sym" $ do 103 | calldata' <- symCalldata "transfer(address,uint256)" [AbiAddressType, AbiUIntType 256] ["0xdead"] (AbstractBuf "txdata") 104 | let 105 | blockNum = 16198552 106 | postc _ (Failure _ _ (Revert _)) = PBool False 107 | postc _ _ = PBool True 108 | vm <- liftIO $ weth9VM blockNum calldata' 109 | (_, [Cex (_, model)]) <- withSolvers Z3 1 1 Nothing $ \solvers -> 110 | verify solvers (rpcVeriOpts (BlockNumber blockNum, testRpc)) (symbolify vm) (Just postc) 111 | liftIO $ assertBool "model should exceed caller balance" (getVar model "arg2" >= 695836005599316055372648) 112 | ] 113 | ] 114 | 115 | -- call into WETH9 from 0xf04a... (a large holder) 116 | weth9VM :: W256 -> (Expr Buf, [Prop]) -> IO (VM Concrete RealWorld) 117 | weth9VM blockNum calldata' = do 118 | let 119 | caller' = LitAddr 0xf04a5cc80b1e94c69b48f5ee68a08cd2f09a7c3e 120 | weth9 = Addr 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 121 | callvalue' = Lit 0 122 | vmFromRpc blockNum calldata' callvalue' caller' weth9 123 | 124 | vmFromRpc :: W256 -> (Expr Buf, [Prop]) -> Expr EWord -> Expr EAddr -> Addr -> IO (VM Concrete RealWorld) 125 | vmFromRpc blockNum calldata callvalue caller address = do 126 | ctrct <- fetchContractFrom (BlockNumber blockNum) testRpc address >>= \case 127 | Nothing -> internalError $ "contract not found: " <> show address 128 | Just contract' -> pure contract' 129 | 130 | blk <- fetchBlockFrom (BlockNumber blockNum) testRpc >>= \case 131 | Nothing -> internalError "could not fetch block" 132 | Just b -> pure b 133 | 134 | stToIO $ (makeVm $ VMOpts 135 | { contract = ctrct 136 | , otherContracts = [] 137 | , calldata = calldata 138 | , value = callvalue 139 | , address = LitAddr address 140 | , caller = caller 141 | , origin = LitAddr 0xacab 142 | , gas = 0xffffffffffffffff 143 | , gaslimit = 0xffffffffffffffff 144 | , baseFee = blk.baseFee 145 | , priorityFee = 0 146 | , coinbase = blk.coinbase 147 | , number = blk.number 148 | , timestamp = blk.timestamp 149 | , blockGaslimit = blk.gaslimit 150 | , gasprice = 0 151 | , maxCodeSize = blk.maxCodeSize 152 | , prevRandao = blk.prevRandao 153 | , schedule = blk.schedule 154 | , chainId = 1 155 | , create = False 156 | , baseState = EmptyBase 157 | , txAccessList = mempty 158 | , allowFFI = False 159 | , freshAddresses = 0 160 | , beaconRoot = 0 161 | }) <&> set (#cache % #fetched % at address) (Just ctrct) 162 | 163 | testRpc :: Text 164 | testRpc = "https://eth-mainnet.alchemyapi.io/v2/vpeKFsEF6PHifHzdtcwXSDbhV3ym5Ro4" 165 | 166 | testRpcInfo :: RpcInfo 167 | testRpcInfo = Just (BlockNumber 16198552, testRpc) 168 | -------------------------------------------------------------------------------- /test/scripts/convert_trace_to_json.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euxo pipefail 3 | 4 | num=$(awk 'END{print NR}' "$1") 5 | awk -v ll="$num" 'BEGIN {print "{\"trace\": [" } {if (NR!=ll) {print $0; if (NR!=ll-1) {print ","}} else {print "], \"output\":" $0}} END{print "}"}' "$1" > "$1.json" 6 | --------------------------------------------------------------------------------