diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index b4350210..699095e0 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,2 +1,3 @@ # Migrate code style to Black 449404c7a2b318b8203dbccbeac11c87d20c422f +00cc14c20659cbf717594451dfc7906b295dca98 diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml index 9065b5e0..81e6a948 100644 --- a/.github/workflows/black.yml +++ b/.github/workflows/black.yml @@ -6,5 +6,5 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: psf/black@stable diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b9ce9101..387e17c1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -5,9 +5,10 @@ on: jobs: pypi: runs-on: ubuntu-latest + environment: release steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Build diff --git a/.github/workflows/test-external.yml b/.github/workflows/test-external.yml new file mode 100644 index 00000000..50a9074d --- /dev/null +++ b/.github/workflows/test-external.yml @@ -0,0 +1,71 @@ +name: Test external projects + +on: + push: + branches: [main] +# pull_request: +# branches: [main] + workflow_dispatch: + +jobs: + test: + runs-on: macos-latest + + strategy: + fail-fast: false + matrix: + include: + - repo: "morpho-org/morpho-data-structures" + dir: "morpho-data-structures" + cmd: "halmos --function testProve --loop 4 --symbolic-storage" + branch: "" + - repo: "a16z/cicada" + dir: "cicada" + cmd: "halmos --contract LibUint1024Test --function testProve --loop 256" + branch: "" + - repo: "a16z/cicada" + dir: "cicada" + cmd: "halmos --contract LibPrimeTest --function testProve --loop 256" + branch: "" + - repo: "farcasterxyz/contracts" + dir: "farcaster-contracts" + cmd: "halmos" + branch: "" + - repo: "zobront/halmos-solady" + dir: "halmos-solady" + cmd: "halmos --function testCheck" + branch: "" + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + path: halmos + # we won't be needing tests/lib for this workflow + submodules: false + + - name: Checkout external repo + uses: actions/checkout@v4 + with: + repository: ${{ matrix.repo }} + path: ${{ matrix.dir }} + ref: ${{ matrix.branch }} + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install dependencies + run: python -m pip install --upgrade pip + + - name: Install Halmos + run: python -m pip install -e ./halmos + + - name: Test external repo + run: ${{ matrix.cmd }} -v -st --solver-timeout-assertion 0 --solver-threads 2 + working-directory: ${{ matrix.dir }} diff --git a/.github/workflows/test-ffi.yml b/.github/workflows/test-ffi.yml new file mode 100644 index 00000000..b0f79c31 --- /dev/null +++ b/.github/workflows/test-ffi.yml @@ -0,0 +1,41 @@ +name: Test FFI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: macos-latest + + strategy: + fail-fast: false + matrix: + include: + - testname: "tests/ffi" + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install pytest + + - name: Install Halmos + run: python -m pip install -e . + + - name: Run pytest + run: pytest -v tests/test_halmos.py -k ${{ matrix.testname }} --halmos-options="--ffi -v -st --solver-timeout-assertion 0" diff --git a/.github/workflows/test-long.yml b/.github/workflows/test-long.yml new file mode 100644 index 00000000..c7d1501e --- /dev/null +++ b/.github/workflows/test-long.yml @@ -0,0 +1,45 @@ +name: Test long + +on: + push: + branches: [main] +# pull_request: +# branches: [main] + workflow_dispatch: + +jobs: + test: + runs-on: macos-latest + + strategy: + fail-fast: false + matrix: + include: + - testname: "tests/solver" + - testname: "examples/simple" + - testname: "examples/tokens/ERC20" + - testname: "examples/tokens/ERC721" + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install pytest + + - name: Install Halmos + run: python -m pip install -e . + + - name: Run pytest + run: pytest -x -v tests/test_halmos.py -k ${{ matrix.testname }} --halmos-options="-v -st --solver-timeout-assertion 0 --solver-threads 2" -s --log-cli-level= diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b6d92b33..1c30b7b3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,17 +11,15 @@ jobs: runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: - os: ["ubuntu-latest", "windows-2022"] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.8.13", "3.9.13", "3.10.6"] - parallel: ["", "--test-parallel", "--solver-parallel", "--test-parallel --solver-parallel"] - exclude: - # python 3.8.13 is not available in windows-2022 - - os: "windows-2022" - python-version: "3.8.13" + os: ["macos-latest", "ubuntu-latest", "windows-latest"] + python-version: ["3.9", "3.10", "3.11"] + parallel: ["", "--test-parallel"] + storage-layout: ["solidity", "generic"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive @@ -29,20 +27,17 @@ jobs: uses: foundry-rs/foundry-toolchain@v1 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pytest + python -m pip install pytest - name: Install Halmos - run: pip install -e . + run: python -m pip install -e . - name: Run pytest - run: pytest - - - name: Test Halmos - run: halmos --root tests --symbolic-storage --function test ${{ matrix.parallel }} + run: pytest -v -k "not long and not ffi" --ignore=tests/lib --halmos-options="-v -st ${{ matrix.parallel }} --storage-layout ${{ matrix.storage-layout }} --solver-timeout-assertion 0" diff --git a/.gitignore b/.gitignore index 6e2c9337..06389977 100644 --- a/.gitignore +++ b/.gitignore @@ -19,5 +19,5 @@ env/ venv/ # Foundry build files -*/cache/ -*/out/ +cache/ +out/ diff --git a/.gitmodules b/.gitmodules index 589dc660..881921c8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,18 @@ [submodule "tests/lib/forge-std"] path = tests/lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "tests/lib/halmos-cheatcodes"] + path = tests/lib/halmos-cheatcodes + url = https://github.com/a16z/halmos-cheatcodes +[submodule "tests/lib/openzeppelin-contracts"] + path = tests/lib/openzeppelin-contracts + url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "tests/lib/solmate"] + path = tests/lib/solmate + url = https://github.com/transmissions11/solmate +[submodule "tests/lib/solady"] + path = tests/lib/solady + url = https://github.com/Vectorized/solady +[submodule "tests/lib/multicaller"] + path = tests/lib/multicaller + url = https://github.com/Vectorized/multicaller diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..1785ac0d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,60 @@ +# Contributing to Halmos + +We greatly appreciate your feedback, suggestions, and contributions to make Halmos a better tool for everyone! + +Join the [Halmos Telegram Group][chat] for any inquiries or further discussions. + +[chat]: + +## Development Setup + +If you want to submit a pull request, fork the repository: + +```sh +gh repo fork a16z/halmos +``` + +Or, if you just want to develop locally, clone it: + +```sh +git clone git@github.com:a16z/halmos.git +``` + +Create and activate a virtual environment: + +```sh +python3.11 -m venv .venv +source .venv/bin/activate +``` + +Install the dependencies: + +```sh +python -m pip install -r requirements.txt +python -m pip install -r requirements-dev.txt +``` + +Install and run the git hook scripts: + +```sh +pre-commit install +pre-commit run --all-files +``` + +## Coding Style + +We recommend enabling the [black] formatter in your editor, but you can run it manually if needed: + +```sh +python -m black . +``` + +[black]: + +## License + +By contributing, you agree that your contributions will be licensed under its [AGPL-3.0](LICENSE) License. + +## Disclaimer + +_These smart contracts and code are being provided as is. No guarantee, representation or warranty is being made, express or implied, as to the safety or correctness of the user interface or the smart contracts and code. They have not been audited and as such there can be no assurance they will work as intended, and users may experience delays, failures, errors, omissions or loss of transmitted information. THE SMART CONTRACTS AND CODE CONTAINED HEREIN ARE FURNISHED AS IS, WHERE IS, WITH ALL FAULTS AND WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING ANY WARRANTY OF MERCHANTABILITY, NON-INFRINGEMENT OR FITNESS FOR ANY PARTICULAR PURPOSE. Further, use of any of these smart contracts and code may be restricted or prohibited under applicable law, including securities laws, and it is therefore strongly advised for you to contact a reputable attorney in any jurisdiction where these smart contracts and code may be accessible for any questions or concerns with respect thereto. Further, no information provided in this repo should be construed as investment advice or legal advice for any particular facts or circumstances, and is not meant to replace competent counsel. a16z is not liable for any use of the foregoing, and users should proceed with caution and use at their own risk. See a16z.com/disclosures for more info._ diff --git a/README.md b/README.md index 8bd1425a..6a24aaf2 100644 --- a/README.md +++ b/README.md @@ -3,15 +3,9 @@ [![License](https://img.shields.io/github/license/a16z/halmos)](https://github.com/a16z/halmos/blob/main/LICENSE) [![chat](https://img.shields.io/badge/chat-telegram-blue)](https://t.me/+4UhzHduai3MzZmUx) -_Symbolic Bounded Model Checker for Ethereum Smart Contracts Bytecode_ +Halmos is a _symbolic testing_ tool for EVM smart contracts. A Solidity/Foundry frontend is currently offered by default, with plans to provide support for other languages, such as Vyper and Huff, in the future. -**_Symbolic_:** Halmos executes the given contract bytecode with symbolic function arguments and symbolic storage states, enabling it to systematically explore all possible behaviors of the contract. - -**_Bounded_:** Halmos unrolls loops up to a specified bound and sets the size of variable-length arrays, allowing it to run automatically without the need for additional user annotations. - -**_Model Checking_:** Halmos proves that assertions are never violated by any inputs or provides a counter-example. This allows Halmos to be used for bug detection as well as formal verification of the contract. - -For more information, refer to our post on "_[Symbolic testing with Halmos: Leveraging existing tests for formal verification][post]_." +You can read more in our post: "_[Symbolic testing with Halmos: Leveraging existing tests for formal verification][post]_." Join the [Halmos Telegram Group][chat] for any inquiries or further discussions. @@ -21,94 +15,33 @@ Join the [Halmos Telegram Group][chat] for any inquiries or further discussions. ## Installation ``` -$ pip install halmos +pip install halmos ``` -Or, if you want to try the latest dev version: +Or, if you want to try out the nightly build version: ``` -$ pip install git+https://github.com/a16z/halmos +pip install git+https://github.com/a16z/halmos ``` ## Usage ``` -$ cd /path/to/src -$ halmos +cd /path/to/src +halmos ``` For more details: ``` -$ halmos --help +halmos --help ``` ## Examples -Given a contract, [Example.sol](examples/src/Example.sol): -```solidity -contract Example { - function totalPriceBuggy(uint96 price, uint32 quantity) public pure returns (uint128) { - unchecked { - return uint120(price) * quantity; // buggy type casting: uint120 vs uint128 - } - } -} -``` - -You write some **property-based tests** (in Solidity), [Example.t.sol](examples/test/Example.t.sol): -```solidity -contract ExampleTest is Example { - function testTotalPriceBuggy(uint96 price, uint32 quantity) public pure { - uint128 total = totalPriceBuggy(price, quantity); - assert(quantity == 0 || total >= price); - } -} -``` - -Then you can run **fuzz testing** to quickly check those properties for **some random inputs**: -``` -$ forge test -[PASS] testTotalPriceBuggy(uint96,uint32) (runs: 256, μ: 462, ~: 466) -``` - -Once it passes, you can also perform **symbolic testing** to verify the same properties for **all possible inputs** (up to a specified limit): -``` -$ halmos -[FAIL] testTotalPriceBuggy(uint96,uint32) (paths: 6, time: 0.10s, bounds: []) -Counterexample: [p_price_uint96 = 39614081294025656978550816768, p_quantity_uint32 = 1073741824] -``` - -_(In this specific example, Halmos discovered an input that violated the assertion, which was missed by the fuzzer!)_ - -## Develop - -```sh -# if you want to submit a pull request, fork the repository: - -gh repo fork a16z/halmos - -# or if you just want to develop locally, clone it -# git clone git@github.com:a16z/halmos.git - -# create and activate a virtual environment +Refer to the [getting started guide](docs/getting-started.md) and the [examples](examples/README.md) directory. -python3.11 -m venv .venv -source .venv/bin/activate +## Contributing -# install the dependencies - -python -m pip install -r requirements.txt -python -m pip install -r requirements-dev.txt - -# install and run the git hook scripts - -pre-commit install -pre-commit run --all-files - -# we recommend enabling the black formatter in your editor -# but you can run it manually if needed: - -python -m black . -``` +Refer to the [contributing guidelines](CONTRIBUTING.md). ## Disclaimer diff --git a/benchmarks/baolean.sh b/benchmarks/baolean.sh new file mode 100755 index 00000000..73f2229d --- /dev/null +++ b/benchmarks/baolean.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env sh + +GREEN='\033[0;32m' +NC='\033[0m' # No Color + +if [ ! -d "symexec-bench" ]; then + echo "Cloning symexec-bench..." + git clone --depth 1 -b symtest --single-branch https://github.com/baolean/symexec-bench.git +else + echo "Using existing symexec-bench checkout" +fi + +for test_name in "PostExampleTest" "PostExampleTwoTest" "PostExampleTwoLiveTest" "FooTest" "MiniVatTest"; do + echo + echo -e "▀▄▀▄▀▄ 🎀 Running ${GREEN}${test_name}${NC} 🎀 ▄▀▄▀▄▀" + time halmos --root symexec-bench/SymTest --contract ${test_name} "--function" test +done + diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 00000000..865305d8 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,272 @@ +# How to write symbolic tests with Halmos + +Symbolic tests looks similar to fuzz tests, but there are certain differences that need to be understood. This guide will walk you through the process of writing symbolic tests, highlighting the differences compared to fuzz tests. It is intended for those who are already familiar with [Dapptools]-/[Foundry]-style fuzz tests. If you haven't experienced fuzz tests before, please refer to the [Foundry document][Foundry Fuzz Testing] to grasp the basic concepts. + +[Dapptools]: +[Foundry]: +[Foundry Fuzz Testing]: + +## 0. Install Halmos + +Halmos is available as a [Python package][Halmos Package], and can be installed using `pip`: +``` +pip install halmos +``` + +[Halmos Package]: + +**Tips:** + +- If you want to try out the nightly build version, you can install it from the Github repository: + ``` + pip install git+https://github.com/a16z/halmos + ``` + +- If you're not familiar with managing Python packages, we recommend using `venv`. Create a virtual environment and install Halmos within it: + ``` + python3 -m venv + source /bin/activate + pip install halmos + ``` + You can activate or deactivate the virtual environment before or after using Halmos: + ``` + # to activate: + source /bin/activate + + # to deactivate: + deactivate + ``` + +## 1. Write setUp() + +Similar to foundry tests, you can provide the `setUp()` function that will be executed before each test. In the setup function, you can create an instance of the target contracts, and initialize their state. These initialized contracts will then be accessible for every test. + +Furthermore, you are also allowed to call the constructor with symbolic arguments, initializing the contract state to be symbolic. You can create those symbols using [Halmos cheatcodes]. + +[Halmos cheatcodes]: + +For example, consider a basic ERC20 token contract as shown below: +```solidity +import {ERC20} from "openzeppelin/token/ERC20/ERC20.sol"; + +contract MyToken is ERC20 { + constructor(uint256 initialSupply) ERC20("MyToken", "MT") { + _mint(msg.sender, initialSupply); + } +} +``` +Then you can write a `setUp()` function that creates a new token contract with a _symbolic_ initial supply, as follows: +```solidity +import {SymTest} from "halmos-cheatcodes/SymTest.sol"; + +contract MyTokenTest is SymTest { + MyToken token; + + function setUp() public { + uint256 initialSupply = svm.createUint256('initialSupply'); + token = new MyToken(initialSupply); + } +} +``` +In the above example, `svm.createUint256()` is a symbolic cheatcode that generates a new symbol of type `uint256`. It's important to understand that the created symbol represents a _set_ of all integers within the range of `[0, 2^256 - 1]`, rather than being a random value selected from the range. + +By using the symbolic initial supply, you can check if the given tests pass for all possible initial supply configurations, rather than just a randomly selected supply setup. + +**Tips:** + +- The Halmos cheatcodes can be installed like any other Solidity dependencies: + ``` + forge install a16z/halmos-cheatcodes + ``` + +- The current list of available Halmos cheatcodes can be found [here][halmos-cheatcodes-list]. + +[halmos-cheatcodes-list]: + +## 2. Write symbolic tests + +Symbolic tests are structured similarly to fuzz tests. In most cases, they follow the pattern outlined below: +``` +function check__ ( ) { + // specify input conditions + ... + + // call target contracts + ... + + // check output states + ... +} +``` + +Below is an example symbolic test for the token transfer function: +```solidity +function check_transfer(address sender, address receiver, uint256 amount) public { + // specify input conditions + vm.assume(receiver != address(0)); + vm.assume(token.balance(sender) >= amount); + + // record the current balance of sender and receiver + uint256 balanceOfSender = token.balanceOf(sender); + uint256 balanceOfReceiver = token.balanceOf(receiver); + + // call target contract + vm.prank(sender); + token.transfer(receiver, amount); + + // check output state + assert(token.balanceOf(sender) == balanceOfSender - amount); + assert(token.balanceOf(receiver) == balanceOfReceiver + amount); +} +``` + +We will explain each component using the above test as a running example. + +### 2.1 Declare or create symbolic inputs + +Similar to fuzz tests, you can specify input parameters for each test. + +For instance, our example test declares three input parameters: `sender`, `receiver`, and `amount`, as follows: +```solidity +function check_transfer(address sender, address receiver, uint256 amount) ... +``` + +Unlike fuzz tests, however, in symbolic tests, each input parameter is assigned a symbol that represents all possible values of the given type. In our example, `sender` and `receiver` are assigned an address symbol that ranges from `0x0` to `0xffff...ffff`, and `amount` is assigned an integer symbol ranging over `[0, 2^256-1]`. + +Conceptually, each symbolic test represents a large number of test cases generated by replacing the symbols with every possible input combination. In other words, it's analogous to running an extensive loop as follows:[^symbolic-execution] +```solidity +// conceptual effect of symbolic testing of `check_transfer()` +for (uint160 sender = 0; sender < type(uint160).max; sender++) { + for (uint160 receiver = 0; receiver < type(uint160).max; receiver++) { + for (uint256 amount = 0; amount < type(uint256).max; amount++) { + check_transfer(address(sender), address(receiver), amount); + } + } +} +``` + +[^symbolic-execution]: Note that the number of possible input combinations in our example is `2^160 * 2^160 * 2^256`, and it is computationally infeasible to actually run all of them individually. As a solution, symbolic testing employs the symbolic execution technique, which enables testing all the input combinations without actually running them individually. + +**Tips:** + +- Instead of declaring symbolic input parameters, you can dynamically create symbols inside the test using the Halmos cheatcodes. For instance, our running example can be rewritten as follows: + ```solidity + function check_transfer() { + address sender = svm.createAddress("sender"); + address receiver = svm.createAddress("receiver"); + uint256 amount = svm.createUint256("amount"); + ... + } + ``` + +- Halmos requires dynamically-sized arrays (including `bytes` and `string`) to be given with a fixed size. Thus they cannot be declared as input parameters, but need to be programmatically constructed. For example, a byte array can be generated using the `svm.createBytes()` cheatcode as follows: + ```solidity + bytes memory data = svm.createBytes(96, 'data'); + ``` + Similarly, a dynamic array of integers can be created as shown below: + ```solidity + uint256[] memory arr = new uint256[3]; + for (uint i = 0; i < 3; i++) { + arr[i] = svm.createUint256('element'); + } + ``` + We are planning to implement more cheatcodes and features that can make it easier to declare or create dynamic arrays. + +### 2.2 Specify input conditions + +Recall that symbolic tests take into account all possible input combinations. However, not all input combinations are relevant or valid for every test scenario. Similar to fuzz tests, you can use `vm.assume()` to specify the conditions for valid inputs. + +In our example, the conditions for the valid sender and receiver addresses are specified as follows: +```solidity +vm.assume(receiver != address(0)); +vm.assume(token.balance(sender) >= amount); +``` +Like fuzz tests, any input combinations that don't satisfy the `assume()` conditions are disregarded. This means that, after executing the above `assume()` statements, only the input combinations in which the receiver is non-zero and the sender has sufficient balance are considered. Other input combinations that violate these conditions are ignored. + +**Tips:** + +- You need to be careful not to exclude valid inputs by setting too strong input conditions. + +- In symbolic tests, avoid using `bound()` as it tends to perform poorly. Instead, use `vm.assume()` which is more efficient and enhances readability: + ```solidity + uint256 tokenId = svm.createUint256("tokenId"); + + // ❌ don't do this + tokenId = bound(tokenId, 1, MAX_TOKEN_ID); + + // ✅ do this + vm.assume(1 <= tokenId && tokenId <= MAX_TOKEN_ID); + ``` + +### 2.3 Call target contracts + +Now you can invoke the target contracts with the prepared input symbols. + +In our example, the transfer function is called with the symbolic receiver address and amount. The `prank()` cheatcode is also used to set `msg.sender` to the symbolic sender address, as shown below: +```solidity +vm.prank(sender); +token.transfer(receiver, amount); +``` + +**Tips:** + +- If your goal is to check whether the target contract reverts under the expected conditions, a low-level call should be used. This allows the execution to continue even if the external call fails. Below is an example of a low-level call to the token transfer function. Note that the return value `success` can be subsequently used to check the reverting conditions. + ```solidity + vm.prank(sender); + (bool success,) = address(token).call( + abi.encodeWithSelector(token.transfer.selector, receiver, amount) + ); + if (!success) { + // check conditions for transfer failure + } + ``` + +### 2.4 Check output states + +After calling the target contracts, you can write assertions against the output state of the contracts. + +In our example, the following assertions against the output state of the token contract are provided: +```solidity +assert(token.balanceOf(sender) == balanceOfSender - amount); +assert(token.balanceOf(receiver) == balanceOfReceiver + amount); +``` + +If there are any inputs that violate these assertions, Halmos will reports those inputs, referred to as counterexamples. + +For our example, Halmos will identify an input combination where the sender address is identical to the receiver address. This is because self-transfers do not alter the balance, leading to scenarios where the above assertions are not satisfied. + +**Tips:** + +- Halmos focuses solely on assertion violations (i.e., revert with `Panic(1)`), disregarding other revert cases. This means that Halmos doesn't report any inputs that lead to other types of revert. For instance, in our example, any inputs that trigger an overflow in `balanceOfReceiver + amount`, or inputs causing the external contract call to fail will be ignored. To avoid disregarding such inputs, you can utilize an `unchecked` block or a low-level call. + +- If you're using an older compiler version (`< 0.8.0`) that uses the `INVALID` opcode for assertion violation, rather than the `Panic(1)` error code, then Halmos will _not_ report any counterexamples. In that case, you will need to use a custom assertion that reverts with `Panic(1)` upon failure, as shown below: + ```solidity + function myAssert(bool cond) internal pure { + if (!cond) { + assembly { + mstore(0x00, 0x4e487b71) // Panic() + mstore(0x20, 0x01) // 1 + revert(0x1c, 0x24) // revert Panic(1) + } + } + } + ``` + +## Summary + +Similar to fuzz tests, symbolic tests are structured as follows: +- Declaration of test input parameters. +- Specification of conditions for valid inputs. +- Invocation of the target contracts. +- Assertions regarding the expected output states. + +However, since symbolic tests are performed symbolically, certain behavioral differences need to be considered: +- Test inputs are assigned symbolics, rather than random values. +- Only assertion violations, that is, `Panic(1)` errors, are reported, whereas other errors such as arithmetic overflows are disregarded. +- The `vm.assume()` cheatcode performs better than `bound()`. + +For further insights, refer to [examples of symbolic tests](../examples/README.md). + +Join the [Halmos Telegram Group] for any inquiries or further discussions. + +[Halmos Telegram Group]: diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..3cde5d4f --- /dev/null +++ b/examples/README.md @@ -0,0 +1,31 @@ +# Halmos Examples + +#### Usage Examples + +- [Simple examples](simple/README.md) +- [ERC20](tokens/ERC20/): verifying OpenZeppelin and Solmate ERC20 tokens, and finding the DEI token bug exploited by the [Deus DAO hack](https://rekt.news/deus-dao-r3kt/). +- [ERC721](tokens/ERC721/): verifying OpenZeppelin and Solmate ERC721 tokens. + +#### Halmos Tests in External Projects + +- [Morpho Data Structures] ([TestProveLogarithmicBuckets]): verifying Morpho's complex data structure. +- [Cicada] ([LibPrimeTest], [LibUint1024Test]): verifying Cicada's big (1024-bit) number arithmetic library. +- [Farcaster] ([IdRegistrySymTest], [KeyRegistrySymTest]): verifying the state machine invariants of Farcaster onchain registry contracts. +- [Solady Verification]: verifying the fixed-point math library of Solady. + +[Morpho Data Structures]: +[TestProveLogarithmicBuckets]: + +[Cicada]: +[LibPrimeTest]: +[LibUint1024Test]: + +[Farcaster]: +[IdRegistrySymTest]: +[KeyRegistrySymTest]: + +[Solady Verification]: + +## Disclaimer + +_These smart contracts and code are being provided as is. No guarantee, representation or warranty is being made, express or implied, as to the safety or correctness of the user interface or the smart contracts and code. They have not been audited and as such there can be no assurance they will work as intended, and users may experience delays, failures, errors, omissions or loss of transmitted information. THE SMART CONTRACTS AND CODE CONTAINED HEREIN ARE FURNISHED AS IS, WHERE IS, WITH ALL FAULTS AND WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING ANY WARRANTY OF MERCHANTABILITY, NON-INFRINGEMENT OR FITNESS FOR ANY PARTICULAR PURPOSE. Further, use of any of these smart contracts and code may be restricted or prohibited under applicable law, including securities laws, and it is therefore strongly advised for you to contact a reputable attorney in any jurisdiction where these smart contracts and code may be accessible for any questions or concerns with respect thereto. Further, no information provided in this repo should be construed as investment advice or legal advice for any particular facts or circumstances, and is not meant to replace competent counsel. a16z is not liable for any use of the foregoing, and users should proceed with caution and use at their own risk. See a16z.com/disclosures for more info._ diff --git a/examples/foundry.toml b/examples/foundry.toml deleted file mode 100644 index 90a2b8c8..00000000 --- a/examples/foundry.toml +++ /dev/null @@ -1,6 +0,0 @@ -[profile.default] -src = 'src' -out = 'out' -libs = ['lib'] - -# See more config options https://github.com/foundry-rs/foundry/tree/master/config diff --git a/examples/simple/README.md b/examples/simple/README.md new file mode 100644 index 00000000..0ed140b1 --- /dev/null +++ b/examples/simple/README.md @@ -0,0 +1,41 @@ +# Simple Examples + +Given a contract: +```solidity +contract Example { + function totalPriceBuggy(uint96 price, uint32 quantity) public pure returns (uint128) { + unchecked { + return uint120(price) * quantity; // buggy type casting: uint120 vs uint128 + } + } +} +``` + +You write some **property-based tests** (in Solidity): +```solidity +contract ExampleTest is Example { + function testTotalPriceBuggy(uint96 price, uint32 quantity) public pure { + uint128 total = totalPriceBuggy(price, quantity); + assert(quantity == 0 || total >= price); + } +} +``` + +Then you can run **fuzz testing** to quickly check those properties for **some random inputs**: +``` +$ forge test +[PASS] testTotalPriceBuggy(uint96,uint32) (runs: 256, μ: 462, ~: 466) +``` + +Once it passes, you can also perform **symbolic testing** to verify the same properties for **all possible inputs** (up to a specified limit): +``` +$ halmos --function test +[FAIL] testTotalPriceBuggy(uint96,uint32) (paths: 6, time: 0.10s, bounds: []) +Counterexample: [p_price_uint96 = 39614081294025656978550816768, p_quantity_uint32 = 1073741824] +``` + +_(In this specific example, Halmos discovered an input that violated the assertion, which was missed by the fuzzer!)_ + +## Disclaimer + +_These smart contracts and code are being provided as is. No guarantee, representation or warranty is being made, express or implied, as to the safety or correctness of the user interface or the smart contracts and code. They have not been audited and as such there can be no assurance they will work as intended, and users may experience delays, failures, errors, omissions or loss of transmitted information. THE SMART CONTRACTS AND CODE CONTAINED HEREIN ARE FURNISHED AS IS, WHERE IS, WITH ALL FAULTS AND WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING ANY WARRANTY OF MERCHANTABILITY, NON-INFRINGEMENT OR FITNESS FOR ANY PARTICULAR PURPOSE. Further, use of any of these smart contracts and code may be restricted or prohibited under applicable law, including securities laws, and it is therefore strongly advised for you to contact a reputable attorney in any jurisdiction where these smart contracts and code may be accessible for any questions or concerns with respect thereto. Further, no information provided in this repo should be construed as investment advice or legal advice for any particular facts or circumstances, and is not meant to replace competent counsel. a16z is not liable for any use of the foregoing, and users should proceed with caution and use at their own risk. See a16z.com/disclosures for more info._ diff --git a/examples/simple/foundry.toml b/examples/simple/foundry.toml new file mode 100644 index 00000000..e32f5716 --- /dev/null +++ b/examples/simple/foundry.toml @@ -0,0 +1,9 @@ +[profile.default] +src = 'src' +out = 'out' +libs = ["../../tests/lib", 'lib'] + +# See more config options https://github.com/foundry-rs/foundry/tree/master/config + +# compile options used by halmos (to prevent unnecessary recompilation when running forge test and halmos together) +extra_output = ["storageLayout", "metadata"] diff --git a/examples/simple/remappings.txt b/examples/simple/remappings.txt new file mode 100644 index 00000000..37cf11ec --- /dev/null +++ b/examples/simple/remappings.txt @@ -0,0 +1 @@ +multicaller/=../../tests/lib/multicaller/src/ diff --git a/examples/simple/src/IsPowerOfTwo.sol b/examples/simple/src/IsPowerOfTwo.sol new file mode 100644 index 00000000..b76ee60f --- /dev/null +++ b/examples/simple/src/IsPowerOfTwo.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +contract IsPowerOfTwo { + + function isPowerOfTwo(uint x) public pure returns (bool) { + unchecked { + return x != 0 && (x & (x - 1)) == 0; + } + } + + function isPowerOfTwoIter(uint x) public pure returns (bool) { + unchecked { + while (x != 0 && (x & 1) == 0) x >>= 1; // NOTE: `--loop 256` option needed for complete verification + return x == 1; + } + } + +} diff --git a/examples/src/Example.sol b/examples/simple/src/TotalPrice.sol similarity index 59% rename from examples/src/Example.sol rename to examples/simple/src/TotalPrice.sol index 3b151deb..e14e0e34 100644 --- a/examples/src/Example.sol +++ b/examples/simple/src/TotalPrice.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity >=0.8.0 <0.9.0; -contract Example { +contract TotalPrice { function totalPriceBuggy(uint96 price, uint32 quantity) public pure returns (uint128) { unchecked { @@ -21,17 +21,4 @@ contract Example { } } - function isPowerOfTwo(uint x) public pure returns (bool) { - unchecked { - return x != 0 && (x & (x - 1)) == 0; - } - } - - function isPowerOfTwoIter(uint x) public pure returns (bool) { - unchecked { - while (x != 0 && (x & 1) == 0) x >>= 1; // NOTE: `--loop 256` option needed for complete verification - return x == 1; - } - } - } diff --git a/examples/simple/src/Vault.sol b/examples/simple/src/Vault.sol new file mode 100644 index 00000000..d40cf314 --- /dev/null +++ b/examples/simple/src/Vault.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +contract Vault { + uint public totalAssets; + uint public totalShares; + + function deposit(uint assets) public returns (uint shares) { + shares = (assets * totalShares) / totalAssets; + + totalAssets += assets; + totalShares += shares; + } + + function mint(uint shares) public returns (uint assets) { + assets = (shares * totalAssets) / totalShares; // buggy + + totalAssets += assets; + totalShares += shares; + } +} diff --git a/examples/simple/test/Fork.t.sol b/examples/simple/test/Fork.t.sol new file mode 100644 index 00000000..79b8340e --- /dev/null +++ b/examples/simple/test/Fork.t.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import "forge-std/Test.sol"; + +contract Counter { + uint public total; // slot 0 + + mapping (address => uint) public map; // slot 1 + + function increment(address user) public { + map[user]++; + total++; + } +} + +contract EmptyContract { } + +contract CounterForkTest is Test { + Counter counter; + + // slot numbers found in "storageLayout" of Counter.json + uint counter_total_slot = 0; + uint counter_map_slot = 1; + + function setUp() public { + // create a new (empty) contract + counter = Counter(address(new EmptyContract())); + + // set the bytecode of `counter` to the given code + vm.etch(address(counter), hex"608060405234801561000f575f80fd5b506004361061003f575f3560e01c80632ddbd13a1461004357806345f43dd81461005d578063b721ef6e14610072575b5f80fd5b61004b5f5481565b60405190815260200160405180910390f35b61007061006b3660046100cf565b610091565b005b61004b6100803660046100cf565b60016020525f908152604090205481565b6001600160a01b0381165f9081526001602052604081208054916100b4836100fc565b90915550505f805490806100c7836100fc565b919050555050565b5f602082840312156100df575f80fd5b81356001600160a01b03811681146100f5575f80fd5b9392505050565b5f6001820161011957634e487b7160e01b5f52601160045260245ffd5b506001019056fea26469706673582212202ef0183898a1560805c26d8e270f79f0c451b549a3d09da92d110096d1deffec64736f6c63430008150033"); + + // set the storage slots to the given values + vm.store(address(counter), bytes32(counter_total_slot), bytes32(uint(12))); // counter.total = 12 + vm.store(address(counter), keccak256(abi.encode(address(0x1001), counter_map_slot)), bytes32(uint(7))); // counter.map[0x1001] = 7 + vm.store(address(counter), keccak256(abi.encode(address(0x1002), counter_map_slot)), bytes32(uint(5))); // counter.map[0x1002] = 5 + + /* NOTE: do _not_ use the keccak256 hash images as the slot number, since keccak256 is interpreted differently during symbolic execution + vm.store(address(counter), bytes32(0xf04c2c5f6f9b62a2b5225d778c263b65e9f9e981a3c2cee9583d90b6a62a361c), bytes32(uint(7))); // counter.map[0x1001] = 7 + vm.store(address(counter), bytes32(0x292339123265925891d3d1c06602cc560d8bb722fcb2db8d37c0fc7a3456fc09), bytes32(uint(5))); // counter.map[0x1002] = 5 + */ + } + + function check_setup() public { + assertEq(counter.total(), 12); + assertEq(counter.map(address(0x1001)), 7); + assertEq(counter.map(address(0x1002)), 5); + assertEq(counter.map(address(0x1003)), 0); // uninitialized storage slots default to a zero value + } + + function check_invariant(address user) public { + assertLe(counter.map(user), counter.total()); + } +} diff --git a/examples/simple/test/IsPowerOfTwo.t.sol b/examples/simple/test/IsPowerOfTwo.t.sol new file mode 100644 index 00000000..17e2ec57 --- /dev/null +++ b/examples/simple/test/IsPowerOfTwo.t.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import "../src/IsPowerOfTwo.sol"; + +/// @custom:halmos --solver-timeout-assertion 0 +contract IsPowerOfTwoTest { + IsPowerOfTwo target; + + function setUp() public { + target = new IsPowerOfTwo(); + } + + function check_isPowerOfTwo_small(uint8 x) public view { + bool result1 = target.isPowerOfTwo(x); + bool result2 = x == 1 || x == 2 || x == 4 || x == 8 || x == 16 || x == 32 || x == 64 || x == 128; + assert(result1 == result2); + } + + /// @custom:halmos --loop 256 + function check_isPowerOfTwo(uint256 x) public view { + bool result1 = target.isPowerOfTwo(x); + bool result2 = false; + for (uint i = 0; i < 256; i++) { // NOTE: `--loop 256` option needed for complete verification + if (x == 2**i) { + result2 = true; + break; + } + } + assert(result1 == result2); + } + + /// @custom:halmos --loop 256 + function check_eq_isPowerOfTwo_isPowerOfTwoIter(uint x) public view { + bool result1 = target.isPowerOfTwo(x); + bool result2 = target.isPowerOfTwoIter(x); + assert(result1 == result2); + } +} diff --git a/examples/simple/test/Multicaller.t.sol b/examples/simple/test/Multicaller.t.sol new file mode 100644 index 00000000..d4cdd2fb --- /dev/null +++ b/examples/simple/test/Multicaller.t.sol @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {MulticallerWithSender} from "multicaller/MulticallerWithSender.sol"; + +import "forge-std/Test.sol"; +import {SymTest} from "halmos-cheatcodes/SymTest.sol"; + +// An unoptimized reference implementation of MulticallerWithSender. +// This serves as a baseline for comparison against the optimized version, to verify the correctness of optimizations. +contract MulticallerWithSenderSpec { + error ArrayLengthsMismatch(); + + error Reentrancy(); + + address public sender; + bool public reentrancyUnlocked; + + constructor() payable { + reentrancyUnlocked = true; + } + + fallback(bytes calldata data) external payable returns (bytes memory) { + if (data.length > 0) revert(); + return abi.encode(sender); + } + + function aggregateWithSender( + address[] calldata targets, + bytes[] calldata data, + uint256[] calldata values + ) external payable returns (bytes[] memory) { + if (targets.length != data.length || data.length != values.length) { + revert ArrayLengthsMismatch(); + } + + if (!reentrancyUnlocked) { + revert Reentrancy(); + } + + bytes[] memory results = new bytes[](data.length); + + if (data.length == 0) { + return results; + } + + // Lock + sender = msg.sender; + reentrancyUnlocked = false; + + for (uint i = 0; i < data.length; i++) { + (bool success, bytes memory retdata) = targets[i].call{value: values[i]}(data[i]); + if (!success) { + _revertWithReturnData(); + } + results[i] = retdata; + } + + // Unlock + sender = address(0); + reentrancyUnlocked = true; + + return results; + } + + function _revertWithReturnData() internal pure { + assembly { + returndatacopy(0, 0, returndatasize()) + revert(0, returndatasize()) + } + } +} + +contract MulticallerWithSenderMock is MulticallerWithSender { + // Provide public getters for the storage variables. + // Note: the variable order is set to align with the packing scheme used by the implementation. + address public sender; + bool public reentrancyUnlocked; +} + +// A mock target contract that keeps track of external calls made via Multicaller +contract TargetMock is SymTest { + // Record of values received from each caller. + mapping (address => uint) private balanceOf; + + fallback(bytes calldata data) external payable returns (bytes memory) { + balanceOf[msg.sender] += msg.value; + + // Simulate deterministically random behaviors. + uint256 mode = msg.value & 255; + if (mode == 0) { + // Call multicaller fallback which should return the multicaller sender. + (bool success, bytes memory retdata) = msg.sender.call(""); + return abi.encode(success, retdata); + } else if (mode == 1) { + // Reenter multicaller aggregateWithSender, which should revert. + (bool success, bytes memory retdata) = msg.sender.call(abi.encodeWithSelector(MulticallerWithSender.aggregateWithSender.selector, new address[](0), new bytes[](0), new uint256[](0))); + return abi.encode(success, retdata); + } else if (mode == 2) { + // Return the callvalue and calldata, which can then be retrieved later when checking the results of multicalls. + return abi.encode(msg.value, data); + } else { + revert(); + } + } +} + +// Check equivalence between the implementation and the reference spec. +// Establishing equivalence ensures that no mistakes are made in the optimizations made by the implementation. +contract MulticallerWithSenderSymTest is SymTest, Test { + MulticallerWithSenderMock impl; // implementation + MulticallerWithSenderSpec spec; // reference spec + + // Slot number of the `balanceOf` mapping in TargetMock. + uint private constant _BALANCEOF_SLOT = 1; + + address[] targetMocks; + + function setUp() public { + impl = new MulticallerWithSenderMock(); + spec = new MulticallerWithSenderSpec(); + + assert(impl.sender() == spec.sender()); + assert(impl.reentrancyUnlocked() == spec.reentrancyUnlocked()); + + vm.deal(address(this), 100_000_000 ether); + vm.assume(address(impl).balance == address(spec).balance); + } + + function _check_equivalence(bytes memory data) internal { + uint value = svm.createUint256("value"); + + (bool success_impl, bytes memory retdata_impl) = address(impl).call{value: value}(data); + (bool success_spec, bytes memory retdata_spec) = address(spec).call{value: value}(data); + + // Check: `impl` succeeds if and only if `spec` succeeds. + assert(success_impl == success_spec); + // Check: the return data must be identical. + assert(keccak256(retdata_impl) == keccak256(retdata_spec)); + + // Check: the storage states must remain the same. + assert(impl.sender() == spec.sender()); + assert(impl.reentrancyUnlocked() == spec.reentrancyUnlocked()); + + // Check: the remaining balances must be equal. + assert(address(impl).balance == address(spec).balance); + // Check: the total amounts sent to each target must be equal. + for (uint i = 0; i < targetMocks.length; i++) { + bytes32 target_balance_impl = vm.load(targetMocks[i], keccak256(abi.encode(impl, _BALANCEOF_SLOT))); + bytes32 target_balance_spec = vm.load(targetMocks[i], keccak256(abi.encode(spec, _BALANCEOF_SLOT))); + assert(target_balance_impl == target_balance_spec); + } + } + + // Generate input arguments for `aggregateWithSender()`, given the specific sizes of dynamic arrays. + function _create_inputs( + uint targets_length, + uint data_length, + uint values_length, + uint data_size + ) internal returns (bytes memory) { + // Construct `address[] targets` where `target[i]` may or may not be aliased with `target[i-1]`. + // This results in 2^(n-1) combinations of `targets` arrays, covering various alias scenarios. + address[] memory targets = new address[](targets_length); + for (uint i = 0; i < targets_length; i++) { + if (i == 0 || svm.createBool("unique_targets[i]")) { + address targetMock = address(new TargetMock()); + targetMocks.push(targetMock); + targets[i] = targetMock; + } else { + targets[i] = targets[i-1]; // alias + } + } + + // Construct `bytes[] data`, where `bytes data[i]` is created with the given `data_size`. + bytes[] memory data = new bytes[](data_length); + for (uint i = 0; i < data_length; i++) { + data[i] = svm.createBytes(data_size, "data[i]"); + } + + // Construct `uint256[] values`. + uint256[] memory values = new uint256[](values_length); + for (uint i = 0; i < values_length; i++) { + values[i] = svm.createUint256("values[i]"); + } + + return abi.encodeWithSelector(MulticallerWithSender.aggregateWithSender.selector, targets, data, values); + } + + // + // Instantiations of the `_check_equivalence()` test for various combinations of dynamic array sizes. + // + + function check_fallback_0() public { _check_equivalence(""); } + function check_fallback_1() public { _check_equivalence("1"); } + + function check_1_0_0_1() public { _check_equivalence(_create_inputs(1, 0, 0, 1)); } + function check_0_0_0_1() public { _check_equivalence(_create_inputs(0, 0, 0, 1)); } + function check_1_1_1_1() public { _check_equivalence(_create_inputs(1, 1, 1, 1)); } + function check_2_2_2_1() public { _check_equivalence(_create_inputs(2, 2, 2, 1)); } + + function check_1_0_0_32() public { _check_equivalence(_create_inputs(1, 0, 0, 32)); } + function check_0_0_0_32() public { _check_equivalence(_create_inputs(0, 0, 0, 32)); } + function check_1_1_1_32() public { _check_equivalence(_create_inputs(1, 1, 1, 32)); } + function check_2_2_2_32() public { _check_equivalence(_create_inputs(2, 2, 2, 32)); } + + function check_1_0_0_31() public { _check_equivalence(_create_inputs(1, 0, 0, 31)); } + function check_0_0_0_31() public { _check_equivalence(_create_inputs(0, 0, 0, 31)); } + function check_1_1_1_31() public { _check_equivalence(_create_inputs(1, 1, 1, 31)); } + function check_2_2_2_31() public { _check_equivalence(_create_inputs(2, 2, 2, 31)); } + + function check_1_0_0_65() public { _check_equivalence(_create_inputs(1, 0, 0, 65)); } + function check_0_0_0_65() public { _check_equivalence(_create_inputs(0, 0, 0, 65)); } + function check_1_1_1_65() public { _check_equivalence(_create_inputs(1, 1, 1, 65)); } + function check_2_2_2_65() public { _check_equivalence(_create_inputs(2, 2, 2, 65)); } +} diff --git a/examples/simple/test/TotalPrice.t.sol b/examples/simple/test/TotalPrice.t.sol new file mode 100644 index 00000000..e1a6ed83 --- /dev/null +++ b/examples/simple/test/TotalPrice.t.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import "../src/TotalPrice.sol"; + +/// @custom:halmos --solver-timeout-assertion 0 +contract TotalPriceTest { + TotalPrice target; + + function setUp() public { + target = new TotalPrice(); + } + + function check_totalPriceBuggy(uint96 price, uint32 quantity) public view { + uint128 total = target.totalPriceBuggy(price, quantity); + assert(quantity == 0 || total >= price); + } + + function check_totalPriceFixed(uint96 price, uint32 quantity) public view { + uint128 total = target.totalPriceFixed(price, quantity); + assert(quantity == 0 || total >= price); + } + + function check_eq_totalPriceFixed_totalPriceConservative(uint96 price, uint32 quantity) public view { + uint128 total1 = target.totalPriceFixed(price, quantity); + uint128 total2 = target.totalPriceConservative(price, quantity); + assert(total1 == total2); + } +} diff --git a/examples/simple/test/Vault.t.sol b/examples/simple/test/Vault.t.sol new file mode 100644 index 00000000..d354ff10 --- /dev/null +++ b/examples/simple/test/Vault.t.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {SymTest} from "halmos-cheatcodes/SymTest.sol"; + +import {Vault} from "../src/Vault.sol"; + +contract VaultMock is Vault { + function setTotalAssets(uint _totalAssets) public { + totalAssets = _totalAssets; + } + + function setTotalShares(uint _totalShares) public { + totalShares = _totalShares; + } +} + +/// @custom:halmos --solver-timeout-assertion 0 +contract VaultTest is SymTest { + VaultMock vault; + + function setUp() public { + vault = new VaultMock(); + + vault.setTotalAssets(svm.createUint256("A1")); + vault.setTotalShares(svm.createUint256("S1")); + } + + /// need to set a timeout for this test, the solver can run for hours + /// @custom:halmos --solver-timeout-assertion 10000 + function check_deposit(uint assets) public { + uint A1 = vault.totalAssets(); + uint S1 = vault.totalShares(); + + vault.deposit(assets); + + uint A2 = vault.totalAssets(); + uint S2 = vault.totalShares(); + + // assert(A1 / S1 <= A2 / S2); + assert(A1 * S2 <= A2 * S1); // no counterexample + } + + function check_mint(uint shares) public { + uint A1 = vault.totalAssets(); + uint S1 = vault.totalShares(); + + vault.mint(shares); + + uint A2 = vault.totalAssets(); + uint S2 = vault.totalShares(); + + // assert(A1 / S1 <= A2 / S2); + assert(A1 * S2 <= A2 * S1); // counterexamples exist + } +} diff --git a/examples/test/Example.t.sol b/examples/test/Example.t.sol deleted file mode 100644 index be457df8..00000000 --- a/examples/test/Example.t.sol +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity >=0.8.0 <0.9.0; - -import "../src/Example.sol"; - -contract ExampleTest is Example { - - function testTotalPriceBuggy(uint96 price, uint32 quantity) public pure { - uint128 total = totalPriceBuggy(price, quantity); - assert(quantity == 0 || total >= price); - } - - function testTotalPriceFixed(uint96 price, uint32 quantity) public pure { - uint128 total = totalPriceFixed(price, quantity); - assert(quantity == 0 || total >= price); - } - - function testTotalPriceFixedEqualsToConservative(uint96 price, uint32 quantity) public pure { - uint128 total1 = totalPriceFixed(price, quantity); - uint128 total2 = totalPriceConservative(price, quantity); - assert(total1 == total2); - } - - function testIsPowerOfTwo(uint8 x) public pure { - bool result1 = isPowerOfTwo(x); - bool result2 = x == 1 || x == 2 || x == 4 || x == 8 || x == 16 || x == 32 || x == 64 || x == 128; - assert(result1 == result2); - } - - function testIsPowerOfTwo(uint256 x) public pure { - bool result1 = isPowerOfTwo(x); - bool result2 = false; - for (uint i = 0; i < 256; i++) { // NOTE: `--loop 256` option needed for complete verification - if (x == 2**i) { - result2 = true; - break; - } - } - assert(result1 == result2); - } - - function testIsPowerOfTwoEq(uint x) public pure { - bool result1 = isPowerOfTwo(x); - bool result2 = isPowerOfTwoIter(x); - assert(result1 == result2); - } - -} diff --git a/examples/tokens/ERC20/foundry.toml b/examples/tokens/ERC20/foundry.toml new file mode 100644 index 00000000..d85bbc96 --- /dev/null +++ b/examples/tokens/ERC20/foundry.toml @@ -0,0 +1,4 @@ +[profile.default] +src = "src" +out = "out" +libs = ["../../../tests/lib", "lib"] diff --git a/examples/tokens/ERC20/remappings.txt b/examples/tokens/ERC20/remappings.txt new file mode 100644 index 00000000..acf3e02d --- /dev/null +++ b/examples/tokens/ERC20/remappings.txt @@ -0,0 +1,2 @@ +openzeppelin/=../../../tests/lib/openzeppelin-contracts/contracts/ +ds-test/=../../../tests/lib/forge-std/lib/ds-test/src/ diff --git a/examples/tokens/ERC20/src/OpenZeppelinERC20.sol b/examples/tokens/ERC20/src/OpenZeppelinERC20.sol new file mode 100644 index 00000000..da1a7170 --- /dev/null +++ b/examples/tokens/ERC20/src/OpenZeppelinERC20.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {ERC20} from "openzeppelin/token/ERC20/ERC20.sol"; + +contract OpenZeppelinERC20 is ERC20 { + constructor(string memory name, string memory symbol, uint256 initialSupply, address deployer) ERC20(name, symbol) { + _mint(deployer, initialSupply); + } +} diff --git a/examples/tokens/ERC20/src/SoladyERC20.sol b/examples/tokens/ERC20/src/SoladyERC20.sol new file mode 100644 index 00000000..03ddd418 --- /dev/null +++ b/examples/tokens/ERC20/src/SoladyERC20.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {ERC20} from "solady/tokens/ERC20.sol"; + +contract SoladyERC20 is ERC20 { + string internal _name; + string internal _symbol; + uint8 internal _decimals; + + constructor( + string memory name_, + string memory symbol_, + uint8 decimals_, + uint256 initialSupply, + address deployer + ) { + _name = name_; + _symbol = symbol_; + _decimals = decimals_; + + _mint(deployer, initialSupply); + } + + function name() public view virtual override returns (string memory) { + return _name; + } + + function symbol() public view virtual override returns (string memory) { + return _symbol; + } + + function decimals() public view virtual override returns (uint8) { + return _decimals; + } +} diff --git a/examples/tokens/ERC20/src/SolmateERC20.sol b/examples/tokens/ERC20/src/SolmateERC20.sol new file mode 100644 index 00000000..a014b68f --- /dev/null +++ b/examples/tokens/ERC20/src/SolmateERC20.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {ERC20} from "solmate/tokens/ERC20.sol"; + +contract SolmateERC20 is ERC20 { + constructor(string memory _name, string memory _symbol, uint8 _decimals, uint256 initialSupply, address deployer) ERC20(_name, _symbol, _decimals) { + _mint(deployer, initialSupply); + } +} diff --git a/examples/tokens/ERC20/test/CurveTokenV3.t.sol b/examples/tokens/ERC20/test/CurveTokenV3.t.sol new file mode 100644 index 00000000..c68645c3 --- /dev/null +++ b/examples/tokens/ERC20/test/CurveTokenV3.t.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {SymTest} from "halmos-cheatcodes/SymTest.sol"; +import {ERC20Test} from "./ERC20Test.sol"; + +// https://github.com/curvefi/curve-contract/blob/master/contracts/tokens/CurveTokenV3.vy + +// Auto-generated by https://bia.is/tools/abi2solidity/ +interface CurveTokenV3 { + function decimals ( ) external view returns ( uint256 ); + function transfer ( address _to, uint256 _value ) external returns ( bool ); + function transferFrom ( address _from, address _to, uint256 _value ) external returns ( bool ); + function approve ( address _spender, uint256 _value ) external returns ( bool ); + function increaseAllowance ( address _spender, uint256 _added_value ) external returns ( bool ); + function decreaseAllowance ( address _spender, uint256 _subtracted_value ) external returns ( bool ); + function mint ( address _to, uint256 _value ) external returns ( bool ); + function burnFrom ( address _to, uint256 _value ) external returns ( bool ); + function set_minter ( address _minter ) external; + function set_name ( string memory _name, string memory _symbol ) external; + function name ( ) external view returns ( string memory ); + function symbol ( ) external view returns ( string memory ); + function balanceOf ( address arg0 ) external view returns ( uint256 ); + function allowance ( address arg0, address arg1 ) external view returns ( uint256 ); + function totalSupply ( ) external view returns ( uint256 ); + function minter ( ) external view returns ( address ); +} + +contract EmptyContract { } + +/// @custom:halmos --storage-layout=generic --solver-timeout-assertion 0 +contract CurveTokenV3Test is ERC20Test { + CurveTokenV3 token_; + address minter; + + /// @custom:halmos --solver-timeout-branching 1000 + function setUp() public override { + token = address(new EmptyContract()); + // Source of Deployed Bytecode: https://etherscan.io/address/0x06325440D014e39736583c165C2963BA99fAf14E#code + vm.etch(token, hex"341561000a57600080fd5b60043610156100185761092e565b600035601c5263313ce567600051141561003957601260005260206000f350005b63a9059cbb60005114156100ee5760043560a01c1561005757600080fd5b60023360e05260c052604060c02080546024358082101561007757600080fd5b80820390509050815550600260043560e05260c052604060c02080546024358181830110156100a557600080fd5b8082019050905081555060243561014052600435337fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef6020610140a3600160005260206000f350005b6323b872dd600051141561023c5760043560a01c1561010c57600080fd5b60243560a01c1561011c57600080fd5b600260043560e05260c052604060c02080546044358082101561013e57600080fd5b80820390509050815550600260243560e05260c052604060c020805460443581818301101561016c57600080fd5b80820190509050815550600360043560e05260c052604060c0203360e05260c052604060c02054610140527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6101405118156101fb5761014051604435808210156101d657600080fd5b80820390509050600360043560e05260c052604060c0203360e05260c052604060c020555b604435610160526024356004357fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef6020610160a3600160005260206000f350005b63095ea7b360005114156102b95760043560a01c1561025a57600080fd5b60243560033360e05260c052604060c02060043560e05260c052604060c0205560243561014052600435337f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9256020610140a3600160005260206000f350005b633950935160005114156103725760043560a01c156102d757600080fd5b60033360e05260c052604060c02060043560e05260c052604060c0205460243581818301101561030657600080fd5b80820190509050610140526101405160033360e05260c052604060c02060043560e05260c052604060c020556101405161016052600435337f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9256020610160a3600160005260206000f350005b63a457c2d760005114156104295760043560a01c1561039057600080fd5b60033360e05260c052604060c02060043560e05260c052604060c02054602435808210156103bd57600080fd5b80820390509050610140526101405160033360e05260c052604060c02060043560e05260c052604060c020556101405161016052600435337f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9256020610160a3600160005260206000f350005b6340c10f1960005114156104e35760043560a01c1561044757600080fd5b600554331461045557600080fd5b6004805460243581818301101561046b57600080fd5b80820190509050815550600260043560e05260c052604060c020805460243581818301101561049957600080fd5b808201905090508155506024356101405260043560007fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef6020610140a3600160005260206000f350005b6379cc679060005114156105995760043560a01c1561050157600080fd5b600554331461050f57600080fd5b600480546024358082101561052357600080fd5b80820390509050815550600260043560e05260c052604060c02080546024358082101561054f57600080fd5b808203905090508155506024356101405260006004357fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef6020610140a3600160005260206000f350005b631652e9fc60005114156105cd5760043560a01c156105b757600080fd5b60055433146105c557600080fd5b600435600555005b63e1430e0660005114156107115760606004356004016101403760406004356004013511156105fb57600080fd5b60406024356004016101c037602060243560040135111561061b57600080fd5b3360206102806004638da5cb5b6102205261023c6005545afa61063d57600080fd5b601f3d1161064a57600080fd5b600050610280511461065b57600080fd5b61014080600060c052602060c020602082510161012060006003818352015b8261012051602002111561068d576106af565b61012051602002850151610120518501555b815160010180835281141561067a575b5050505050506101c080600160c052602060c020602082510161012060006002818352015b826101205160200211156106e757610709565b61012051602002850151610120518501555b81516001018083528114156106d4575b505050505050005b6306fdde0360005114156107ba5760008060c052602060c020610180602082540161012060006003818352015b8261012051602002111561075157610773565b61012051850154610120516020028501525b815160010180835281141561073e575b50505050505061018051806101a001818260206001820306601f82010390500336823750506020610160526040610180510160206001820306601f8201039050610160f350005b6395d89b4160005114156108635760018060c052602060c020610180602082540161012060006002818352015b826101205160200211156107fa5761081c565b61012051850154610120516020028501525b81516001018083528114156107e7575b50505050505061018051806101a001818260206001820306601f82010390500336823750506020610160526040610180510160206001820306601f8201039050610160f350005b6370a08231600051141561089d5760043560a01c1561088157600080fd5b600260043560e05260c052604060c0205460005260206000f350005b63dd62ed3e60005114156108f55760043560a01c156108bb57600080fd5b60243560a01c156108cb57600080fd5b600360043560e05260c052604060c02060243560e05260c052604060c0205460005260206000f350005b6318160ddd60005114156109115760045460005260206000f350005b6307546172600051141561092d5760055460005260206000f350005b5b60006000fd"); + token_ = CurveTokenV3(token); + + minter = token_.minter(); + vm.prank(minter); + token_.mint(address(this), 1_000_000_000e18); + assert(token_.balanceOf(address(this)) == 1_000_000_000e18); + + holders = new address[](3); + holders[0] = address(0x1001); + holders[1] = address(0x1002); + holders[2] = address(0x1003); + + for (uint i = 0; i < holders.length; i++) { + address account = holders[i]; + uint256 balance = svm.createUint256('balance'); + token_.transfer(account, balance); + for (uint j = 0; j < i; j++) { + address other = holders[j]; + uint256 amount = svm.createUint256('amount'); + vm.prank(account); + token_.approve(other, amount); + } + } + } + + function check_NoBackdoor(bytes4 selector, address caller, address other) public { + vm.assume(caller != minter); + vm.assume(selector != CurveTokenV3.set_name.selector); + bytes memory args = svm.createBytes(1024, 'data'); + _checkNoBackdoor(selector, args, caller, other); + } +} diff --git a/examples/tokens/ERC20/test/DEIStablecoin.sol b/examples/tokens/ERC20/test/DEIStablecoin.sol new file mode 100644 index 00000000..65c66486 --- /dev/null +++ b/examples/tokens/ERC20/test/DEIStablecoin.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: AGPL-3.0 +// !! THIS FILE WAS AUTOGENERATED BY abi-to-sol v0.8.0. SEE SOURCE BELOW. !! +// https://gnidan.github.io/abi-to-sol/ +pragma solidity >=0.7.0 <0.9.0; + +interface DEIStablecoin { + + +event Approval( address indexed _owner,address indexed _spender,uint256 _value ) ; +event Initialized( uint8 version ) ; +event LosslessOff( ) ; +event LosslessOn( ) ; +event LosslessTurnOffProposal( uint256 _turnOffDate ) ; +event NewAdmin( address indexed _newAdmin ) ; +event NewRecoveryAdmin( address indexed _newAdmin ) ; +event NewRecoveryAdminProposal( address indexed _candidate ) ; +event NewTimelockPeriod( uint256 _newTimelockPeriod ) ; +event RoleAdminChanged( bytes32 indexed role,bytes32 indexed previousAdminRole,bytes32 indexed newAdminRole ) ; +event RoleGranted( bytes32 indexed role,address indexed account,address indexed sender ) ; +event RoleRevoked( bytes32 indexed role,address indexed account,address indexed sender ) ; +event Transfer( address indexed _from,address indexed _to,uint256 _value ) ; +function BURNER_ROLE( ) external view returns (bytes32 ) ; +function DEFAULT_ADMIN_ROLE( ) external view returns (bytes32 ) ; +function MINTER_ROLE( ) external view returns (bytes32 ) ; +function acceptRecoveryAdminOwnership( bytes memory key ) external ; +function admin( ) external view returns (address ) ; +function allowance( address owner,address spender ) external view returns (uint256 ) ; +function approve( address spender,uint256 amount ) external returns (bool ) ; +function balanceOf( address account ) external view returns (uint256 ) ; +function burn( uint256 amount ) external ; +function burnFrom( address account,uint256 amount ) external ; +function decimals( ) external view returns (uint8 ) ; +function decreaseAllowance( address spender,uint256 subtractedValue ) external returns (bool ) ; +function executeLosslessTurnOff( ) external ; +function executeLosslessTurnOn( ) external ; +function getAdmin( ) external view returns (address ) ; +function getRoleAdmin( bytes32 role ) external view returns (bytes32 ) ; +function grantRole( bytes32 role,address account ) external ; +function hasRole( bytes32 role,address account ) external view returns (bool ) ; +function increaseAllowance( address spender,uint256 addedValue ) external returns (bool ) ; +function initialize( uint256 totalSupply,address admin,address recoveryAdmin,uint256 timelockPeriod,address lossless ) external ; +function isLosslessOn( ) external view returns (bool ) ; +function lossless( ) external view returns (address ) ; +function losslessTurnOffTimestamp( ) external view returns (uint256 ) ; +function mint( address to,uint256 amount ) external ; +function name( ) external view returns (string memory ) ; +function proposeLosslessTurnOff( ) external ; +function recoveryAdmin( ) external view returns (address ) ; +function renounceRole( bytes32 role,address account ) external ; +function revokeRole( bytes32 role,address account ) external ; +function setLosslessAdmin( address newAdmin ) external ; +function setTimelockPeriod( uint256 newTimelockPeriod ) external ; +function supportsInterface( bytes4 interfaceId ) external view returns (bool ) ; +function symbol( ) external view returns (string memory ) ; +function timelockPeriod( ) external view returns (uint256 ) ; +function totalSupply( ) external view returns (uint256 ) ; +function transfer( address recipient,uint256 amount ) external returns (bool ) ; +function transferFrom( address sender,address recipient,uint256 amount ) external returns (bool ) ; +function transferOutBlacklistedFunds( address[] memory from ) external ; +function transferRecoveryAdminOwnership( address candidate,bytes32 keyHash ) external ; +} + + + + +// THIS FILE WAS AUTOGENERATED FROM THE FOLLOWING ABI JSON: +// Source of Contract ABI: https://etherscan.io/address/0x63c28e2ff796e1480eb9ac8c3c55dcb9ae7b3df6#code +/* +[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"_owner","type":"address"},{"indexed":true,"internalType":"address","name":"_spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"_value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint8","name":"version","type":"uint8"}],"name":"Initialized","type":"event"},{"anonymous":false,"inputs":[],"name":"LosslessOff","type":"event"},{"anonymous":false,"inputs":[],"name":"LosslessOn","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"_turnOffDate","type":"uint256"}],"name":"LosslessTurnOffProposal","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"_newAdmin","type":"address"}],"name":"NewAdmin","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"_newAdmin","type":"address"}],"name":"NewRecoveryAdmin","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"_candidate","type":"address"}],"name":"NewRecoveryAdminProposal","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"_newTimelockPeriod","type":"uint256"}],"name":"NewTimelockPeriod","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"previousAdminRole","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"newAdminRole","type":"bytes32"}],"name":"RoleAdminChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleGranted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"}],"name":"RoleRevoked","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"_from","type":"address"},{"indexed":true,"internalType":"address","name":"_to","type":"address"},{"indexed":false,"internalType":"uint256","name":"_value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[],"name":"BURNER_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"DEFAULT_ADMIN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MINTER_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"key","type":"bytes"}],"name":"acceptRecoveryAdminOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"admin","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"burn","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"burnFrom","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"subtractedValue","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"executeLosslessTurnOff","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"executeLosslessTurnOn","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"getAdmin","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"}],"name":"getRoleAdmin","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"grantRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"hasRole","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"addedValue","type":"uint256"}],"name":"increaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"totalSupply","type":"uint256"},{"internalType":"address","name":"admin","type":"address"},{"internalType":"address","name":"recoveryAdmin","type":"address"},{"internalType":"uint256","name":"timelockPeriod","type":"uint256"},{"internalType":"address","name":"lossless","type":"address"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"isLosslessOn","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"lossless","outputs":[{"internalType":"contract ILssController","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"losslessTurnOffTimestamp","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"mint","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"proposeLosslessTurnOff","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"recoveryAdmin","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"renounceRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"revokeRole","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newAdmin","type":"address"}],"name":"setLosslessAdmin","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"newTimelockPeriod","type":"uint256"}],"name":"setTimelockPeriod","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"timelockPeriod","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"from","type":"address[]"}],"name":"transferOutBlacklistedFunds","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"candidate","type":"address"},{"internalType":"bytes32","name":"keyHash","type":"bytes32"}],"name":"transferRecoveryAdminOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"}] +*/ diff --git a/examples/tokens/ERC20/test/DEIStablecoin.t.sol b/examples/tokens/ERC20/test/DEIStablecoin.t.sol new file mode 100644 index 00000000..942f9ed0 --- /dev/null +++ b/examples/tokens/ERC20/test/DEIStablecoin.t.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {SymTest} from "halmos-cheatcodes/SymTest.sol"; +import {ERC20Test} from "./ERC20Test.sol"; + +import {DEIStablecoin} from "./DEIStablecoin.sol"; + +contract EmptyContract { } + +contract SymAccount is SymTest { + fallback(bytes calldata) external payable returns (bytes memory) { + if (svm.createBool("?")) { + return svm.createBytes(32, "retdata"); // any primitive return value: bool, address, uintN, bytesN, etc + } else { + revert(); + } + } +} + +/// @notice This example shows how to find the DEI token bug exploited by the Deus DAO hack: https://rekt.news/deus-dao-r3kt/ +/// @custom:halmos --solver-timeout-assertion 0 +contract DEIStablecoinTest is ERC20Test { + DEIStablecoin token_; + address lossless; + + /// @custom:halmos --solver-timeout-branching 1000 + function setUp() public override { + token = address(new EmptyContract()); + // Source of Deployed Bytecode: https://etherscan.io/address/0x63c28e2ff796e1480eb9ac8c3c55dcb9ae7b3df6#code + vm.etch(token, hex"608060405234801561001057600080fd5b50600436106102ad5760003560e01c806361086b001161017b578063a9059cbb116100d8578063d547741f1161008c578063dd62ed3e11610071578063dd62ed3e146105fe578063f55c980f14610644578063f851a4401461065757600080fd5b8063d547741f146105e3578063d6e242b8146105f657600080fd5b8063b5c22877116100bd578063b5c228771461059c578063ccfa214f146105af578063d5391393146105bc57600080fd5b8063a9059cbb14610581578063b38fe9571461059457600080fd5b806393310ffe1161012f57806395d89b411161011457806395d89b411461055e578063a217fddf14610566578063a457c2d71461056e57600080fd5b806393310ffe14610538578063936af9111461054b57600080fd5b806370a082311161016057806370a08231146104a957806379cc6790146104df57806391d14854146104f257600080fd5b806361086b00146104825780636e9960c31461048b57600080fd5b80632ecaf6751161022957806339509351116101dd57806342966c68116101c257806342966c68146104475780635b8a194a1461045a5780635f6529a31461046257600080fd5b8063395093511461042157806340c10f191461043457600080fd5b8063313ce5671161020e578063313ce567146103b557806334f6ebf5146103c457806336568abe1461040e57600080fd5b80632ecaf675146103995780632f2ff15d146103a257600080fd5b806323b872dd11610280578063282c51f311610265578063282c51f31461034a5780632b7c9fd6146103715780632baa3c9e1461038657600080fd5b806323b872dd14610314578063248a9ca31461032757600080fd5b806301ffc9a7146102b257806306fdde03146102da578063095ea7b3146102ef57806318160ddd14610302575b600080fd5b6102c56102c0366004612d2f565b610677565b60405190151581526020015b60405180910390f35b6102e2610710565b6040516102d19190612f0e565b6102c56102fd366004612c5c565b6107a2565b6035545b6040519081526020016102d1565b6102c5610322366004612c21565b61087c565b610306610335366004612cf5565b60009081526071602052604090206001015490565b6103067f3c11d16cbaffd01df69ce1c404f6340ee057498f5f00246190ea54220576a84881565b61038461037f366004612e37565b610a3d565b005b610384610394366004612bd5565b610b75565b610306603c5481565b6103846103b0366004612d0d565b610d19565b604051601281526020016102d1565b603e546103e990610100900473ffffffffffffffffffffffffffffffffffffffff1681565b60405173ffffffffffffffffffffffffffffffffffffffff90911681526020016102d1565b61038461041c366004612d0d565b610d43565b6102c561042f366004612c5c565b610df6565b610384610442366004612c5c565b610efd565b610384610455366004612cf5565b610f31565b610384610f3e565b6038546103e99073ffffffffffffffffffffffffffffffffffffffff1681565b610306603d5481565b603b5473ffffffffffffffffffffffffffffffffffffffff166103e9565b6103066104b7366004612bd5565b73ffffffffffffffffffffffffffffffffffffffff1660009081526033602052604090205490565b6103846104ed366004612c5c565b61109d565b6102c5610500366004612d0d565b600091825260716020908152604080842073ffffffffffffffffffffffffffffffffffffffff93909316845291905290205460ff1690565b610384610546366004612c5c565b6110e9565b610384610559366004612c85565b6111f5565b6102e26113dd565b610306600081565b6102c561057c366004612c5c565b6113ec565b6102c561058f366004612c5c565b61158c565b61038461165b565b6103846105aa366004612d6f565b61181f565b603e546102c59060ff1681565b6103067f9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a681565b6103846105f1366004612d0d565b6119b9565b6103846119de565b61030661060c366004612bef565b73ffffffffffffffffffffffffffffffffffffffff918216600090815260346020908152604080832093909416825291909152205490565b610384610652366004612cf5565b611b92565b603b546103e99073ffffffffffffffffffffffffffffffffffffffff1681565b60007fffffffff0000000000000000000000000000000000000000000000000000000082167f7965db0b00000000000000000000000000000000000000000000000000000000148061070a57507f01ffc9a7000000000000000000000000000000000000000000000000000000007fffffffff000000000000000000000000000000000000000000000000000000008316145b92915050565b60606036805461071f90613030565b80601f016020809104026020016040519081016040528092919081815260200182805461074b90613030565b80156107985780601f1061076d57610100808354040283529160200191610798565b820191906000526020600020905b81548152906001019060200180831161077b57829003601f168201915b5050505050905090565b603e546000908390839060ff161561086657603e54610100900473ffffffffffffffffffffffffffffffffffffffff166347abf3be336040517fffffffff0000000000000000000000000000000000000000000000000000000060e084901b16815273ffffffffffffffffffffffffffffffffffffffff9182166004820152908516602482015260448101849052606401600060405180830381600087803b15801561084d57600080fd5b505af1158015610861573d6000803e3d6000fd5b505050505b610871338686611c61565b506001949350505050565b603e5460009084908490849060ff161561094a57603e54610100900473ffffffffffffffffffffffffffffffffffffffff1663379f5c69336040517fffffffff0000000000000000000000000000000000000000000000000000000060e084901b16815273ffffffffffffffffffffffffffffffffffffffff91821660048201528187166024820152908516604482015260648101849052608401600060405180830381600087803b15801561093157600080fd5b505af1158015610945573d6000803e3d6000fd5b505050505b73ffffffffffffffffffffffffffffffffffffffff8716600090815260346020908152604080832033845290915290205485811015610a10576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602960248201527f4c45524332303a207472616e7366657220616d6f756e7420657863656564732060448201527f616c6c6f77616e6365000000000000000000000000000000000000000000000060648201526084015b60405180910390fd5b610a1b888888611cd0565b610a2f8833610a2a8985612fb4565b611c61565b506001979650505050505050565b6000610a496001611eea565b90508015610a7e57600080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff166101001790555b610af7866040518060400160405280600381526020017f44454900000000000000000000000000000000000000000000000000000000008152506040518060400160405280600381526020017f444549000000000000000000000000000000000000000000000000000000000081525088888888612075565b610aff61220d565b610b0a6000866122a6565b8015610b6d57600080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00ff169055604051600181527f7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb38474024989060200160405180910390a15b505050505050565b60385473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610c0c576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601e60248201527f4c45524332303a204d757374206265207265636f766572792061646d696e00006044820152606401610a07565b603b5473ffffffffffffffffffffffffffffffffffffffff82811691161415610c91576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601f60248201527f4c45524332303a2043616e6e6f74207365742073616d652061646472657373006044820152606401610a07565b60405173ffffffffffffffffffffffffffffffffffffffff8216907f71614071b88dee5e0b2ae578a9dd7b2ebbe9ae832ba419dc0242cd065a290b6c90600090a2603b80547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff92909216919091179055565b600082815260716020526040902060010154610d348161239a565b610d3e83836122a6565b505050565b73ffffffffffffffffffffffffffffffffffffffff81163314610de8576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602f60248201527f416363657373436f6e74726f6c3a2063616e206f6e6c792072656e6f756e636560448201527f20726f6c657320666f722073656c6600000000000000000000000000000000006064820152608401610a07565b610df282826123a4565b5050565b603e546000908390839060ff1615610eba57603e54610100900473ffffffffffffffffffffffffffffffffffffffff1663cf5961bb336040517fffffffff0000000000000000000000000000000000000000000000000000000060e084901b16815273ffffffffffffffffffffffffffffffffffffffff9182166004820152908516602482015260448101849052606401600060405180830381600087803b158015610ea157600080fd5b505af1158015610eb5573d6000803e3d6000fd5b505050505b33600081815260346020908152604080832073ffffffffffffffffffffffffffffffffffffffff8a16845290915290205461087191908790610a2a908890612f5f565b7f9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a6610f278161239a565b610d3e838361245f565b610f3b3382612554565b50565b60385473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610fd5576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601e60248201527f4c45524332303a204d757374206265207265636f766572792061646d696e00006044820152606401610a07565b603e5460ff1615611042576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601b60248201527f4c45524332303a204c6f73736c65737320616c7265616479206f6e00000000006044820152606401610a07565b6000603d819055603e80547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001660011790556040517f1ba3b66404043da8297d0b876fa6464f2cb127edfc6626308046d4503028322b9190a1565b33600081815260346020908152604080832073ffffffffffffffffffffffffffffffffffffffff87168452909152902054906110df908490610a2a8585612fb4565b610d3e8383612554565b60385473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614611180576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601e60248201527f4c45524332303a204d757374206265207265636f766572792061646d696e00006044820152606401610a07565b603980547fffffffffffffffffffffffff00000000000000000000000000000000000000001673ffffffffffffffffffffffffffffffffffffffff8416908117909155603a8290556040517f6c591da8da2f6e69746d7d9ae61c27ee29fbe303798141b4942ae2aef54274b190600090a25050565b603e54610100900473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614611291576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601e60248201527f4c45524332303a204f6e6c79206c6f73736c65737320636f6e747261637400006044820152606401610a07565b806000805b828110156113955760008585838181106112d9577f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b90506020020160208101906112ee9190612bd5565b73ffffffffffffffffffffffffffffffffffffffff8116600090815260336020526040812080549190559091506113258185612f5f565b603e5460405183815291955073ffffffffffffffffffffffffffffffffffffffff610100909104811691908416907fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9060200160405180910390a35050808061138d90613084565b915050611296565b50603e54610100900473ffffffffffffffffffffffffffffffffffffffff16600090815260336020526040812080548392906113d2908490612f5f565b909155505050505050565b60606037805461071f90613030565b603e546000908390839060ff16156114b057603e54610100900473ffffffffffffffffffffffffffffffffffffffff1663568c75a9336040517fffffffff0000000000000000000000000000000000000000000000000000000060e084901b16815273ffffffffffffffffffffffffffffffffffffffff9182166004820152908516602482015260448101849052606401600060405180830381600087803b15801561149757600080fd5b505af11580156114ab573d6000803e3d6000fd5b505050505b33600090815260346020908152604080832073ffffffffffffffffffffffffffffffffffffffff8916845290915290205484811015611571576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f4c45524332303a2064656372656173656420616c6c6f77616e63652062656c6f60448201527f77207a65726f00000000000000000000000000000000000000000000000000006064820152608401610a07565b6115803387610a2a8885612fb4565b50600195945050505050565b603e546000908390839060ff161561165057603e54610100900473ffffffffffffffffffffffffffffffffffffffff16631ffb811f336040517fffffffff0000000000000000000000000000000000000000000000000000000060e084901b16815273ffffffffffffffffffffffffffffffffffffffff9182166004820152908516602482015260448101849052606401600060405180830381600087803b15801561163757600080fd5b505af115801561164b573d6000803e3d6000fd5b505050505b610871338686611cd0565b60385473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16146116f2576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601e60248201527f4c45524332303a204d757374206265207265636f766572792061646d696e00006044820152606401610a07565b603d5461175b576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601c60248201527f4c45524332303a205475726e4f6666206e6f742070726f706f736564000000006044820152606401610a07565b42603d5411156117c7576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601d60248201527f4c45524332303a2054696d65206c6f636b20696e2070726f67726573730000006044820152606401610a07565b603e80547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001690556000603d8190556040517f3eb72350c9c7928d31e9ab450bfff2c159434aa4b82658a7d8eae7f109cb4e7b9190a1565b60395473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16146118b6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601960248201527f4c45524332303a204d7573742062652063616e646974617465000000000000006044820152606401610a07565b603a548151602083012014611927576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601360248201527f4c45524332303a20496e76616c6964206b6579000000000000000000000000006044820152606401610a07565b60395460405173ffffffffffffffffffffffffffffffffffffffff909116907fb94bba6936ec7f75ee931dadf6e1a4d66b43d09b6fa0178fb13df9b77fb5841f90600090a25060398054603880547fffffffffffffffffffffffff000000000000000000000000000000000000000090811673ffffffffffffffffffffffffffffffffffffffff841617909155169055565b6000828152607160205260409020600101546119d48161239a565b610d3e83836123a4565b60385473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614611a75576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601e60248201527f4c45524332303a204d757374206265207265636f766572792061646d696e00006044820152606401610a07565b603d5415611adf576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4c45524332303a205475726e4f666620616c72656164792070726f706f7365646044820152606401610a07565b603e5460ff16611b4b576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601c60248201527f4c45524332303a204c6f73736c65737320616c7265616479206f6666000000006044820152606401610a07565b603c54611b589042612f5f565b603d8190556040519081527f6ca688e6e3ddd707280140b2bf0106afe883689b6c74e68cbd517576dd9c245a9060200160405180910390a1565b60385473ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614611c29576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601e60248201527f4c45524332303a204d757374206265207265636f766572792061646d696e00006044820152606401610a07565b6040518181527fd3fcf9b3a3c0e56c64bbe7d178c180cd10cc7d1d0f96f76fbf30eaf78829fcd49060200160405180910390a1603c55565b73ffffffffffffffffffffffffffffffffffffffff83811660008181526034602090815260408083209487168084529482529182902085905590518481527f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92591015b60405180910390a3505050565b73ffffffffffffffffffffffffffffffffffffffff8316611d73576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f4c45524332303a207472616e736665722066726f6d20746865207a65726f206160448201527f64647265737300000000000000000000000000000000000000000000000000006064820152608401610a07565b73ffffffffffffffffffffffffffffffffffffffff831660009081526033602052604090205481811015611e29576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602760248201527f4c45524332303a207472616e7366657220616d6f756e7420657863656564732060448201527f62616c616e6365000000000000000000000000000000000000000000000000006064820152608401610a07565b611e338282612fb4565b73ffffffffffffffffffffffffffffffffffffffff8086166000908152603360205260408082209390935590851681529081208054849290611e76908490612f5f565b925050819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef84604051611edc91815260200190565b60405180910390a350505050565b60008054610100900460ff1615611fa1578160ff166001148015611f0d5750303b155b611f99576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602e60248201527f496e697469616c697a61626c653a20636f6e747261637420697320616c72656160448201527f647920696e697469616c697a65640000000000000000000000000000000000006064820152608401610a07565b506000919050565b60005460ff808416911610612038576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602e60248201527f496e697469616c697a61626c653a20636f6e747261637420697320616c72656160448201527f647920696e697469616c697a65640000000000000000000000000000000000006064820152608401610a07565b50600080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff001660ff92909216919091179055600190565b919050565b600054610100900460ff1661210c576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602b60248201527f496e697469616c697a61626c653a20636f6e7472616374206973206e6f74206960448201527f6e697469616c697a696e670000000000000000000000000000000000000000006064820152608401610a07565b612116338861245f565b8551612129906036906020890190612b18565b50845161213d906037906020880190612b18565b50603b80547fffffffffffffffffffffffff000000000000000000000000000000000000000090811673ffffffffffffffffffffffffffffffffffffffff9687161790915560388054821694861694909417909355603980549093169092556000603a819055603c91909155603d55603e80547fffffffffffffffffffffff0000000000000000000000000000000000000000001661010092909316919091027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0016919091176001179055505050565b600054610100900460ff166122a4576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602b60248201527f496e697469616c697a61626c653a20636f6e7472616374206973206e6f74206960448201527f6e697469616c697a696e670000000000000000000000000000000000000000006064820152608401610a07565b565b600082815260716020908152604080832073ffffffffffffffffffffffffffffffffffffffff8516845290915290205460ff16610df257600082815260716020908152604080832073ffffffffffffffffffffffffffffffffffffffff85168452909152902080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0016600117905561233c3390565b73ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff16837f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d60405160405180910390a45050565b610f3b8133612739565b600082815260716020908152604080832073ffffffffffffffffffffffffffffffffffffffff8516845290915290205460ff1615610df257600082815260716020908152604080832073ffffffffffffffffffffffffffffffffffffffff8516808552925280832080547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0016905551339285917ff6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b9190a45050565b73ffffffffffffffffffffffffffffffffffffffff82166124dc576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f4c45524332303a206d696e7420746f20746865207a65726f20616464726573736044820152606401610a07565b80603560008282546124ee9190612f5f565b909155505073ffffffffffffffffffffffffffffffffffffffff82166000818152603360209081526040808320805486019055518481527fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef910160405180910390a35050565b73ffffffffffffffffffffffffffffffffffffffff82166125f7576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602160248201527f45524332303a206275726e2066726f6d20746865207a65726f2061646472657360448201527f73000000000000000000000000000000000000000000000000000000000000006064820152608401610a07565b73ffffffffffffffffffffffffffffffffffffffff8216600090815260336020526040902054818110156126ad576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602260248201527f45524332303a206275726e20616d6f756e7420657863656564732062616c616e60448201527f63650000000000000000000000000000000000000000000000000000000000006064820152608401610a07565b73ffffffffffffffffffffffffffffffffffffffff831660009081526033602052604081208383039055603580548492906126e9908490612fb4565b909155505060405182815260009073ffffffffffffffffffffffffffffffffffffffff8516907fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef90602001611cc3565b600082815260716020908152604080832073ffffffffffffffffffffffffffffffffffffffff8516845290915290205460ff16610df2576127918173ffffffffffffffffffffffffffffffffffffffff16601461280b565b61279c83602061280b565b6040516020016127ad929190612e8d565b604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0818403018152908290527f08c379a0000000000000000000000000000000000000000000000000000000008252610a0791600401612f0e565b6060600061281a836002612f77565b612825906002612f5f565b67ffffffffffffffff811115612864577f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6040519080825280601f01601f19166020018201604052801561288e576020820181803683370190505b5090507f3000000000000000000000000000000000000000000000000000000000000000816000815181106128ec577f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a9053507f780000000000000000000000000000000000000000000000000000000000000081600181518110612976577f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a90535060006129b2846002612f77565b6129bd906001612f5f565b90505b6001811115612aa8577f303132333435363738396162636465660000000000000000000000000000000085600f1660108110612a25577f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b1a60f81b828281518110612a62577f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1916908160001a90535060049490941c93612aa181612ffb565b90506129c0565b508315612b11576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820181905260248201527f537472696e67733a20686578206c656e67746820696e73756666696369656e746044820152606401610a07565b9392505050565b828054612b2490613030565b90600052602060002090601f016020900481019282612b465760008555612b8c565b82601f10612b5f57805160ff1916838001178555612b8c565b82800160010185558215612b8c579182015b82811115612b8c578251825591602001919060010190612b71565b50612b98929150612b9c565b5090565b5b80821115612b985760008155600101612b9d565b803573ffffffffffffffffffffffffffffffffffffffff8116811461207057600080fd5b600060208284031215612be6578081fd5b612b1182612bb1565b60008060408385031215612c01578081fd5b612c0a83612bb1565b9150612c1860208401612bb1565b90509250929050565b600080600060608486031215612c35578081fd5b612c3e84612bb1565b9250612c4c60208501612bb1565b9150604084013590509250925092565b60008060408385031215612c6e578182fd5b612c7783612bb1565b946020939093013593505050565b60008060208385031215612c97578182fd5b823567ffffffffffffffff80821115612cae578384fd5b818501915085601f830112612cc1578384fd5b813581811115612ccf578485fd5b8660208260051b8501011115612ce3578485fd5b60209290920196919550909350505050565b600060208284031215612d06578081fd5b5035919050565b60008060408385031215612d1f578182fd5b82359150612c1860208401612bb1565b600060208284031215612d40578081fd5b81357fffffffff0000000000000000000000000000000000000000000000000000000081168114612b11578182fd5b600060208284031215612d80578081fd5b813567ffffffffffffffff80821115612d97578283fd5b818401915084601f830112612daa578283fd5b813581811115612dbc57612dbc6130ec565b604051601f82017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0908116603f01168101908382118183101715612e0257612e026130ec565b81604052828152876020848701011115612e1a578586fd5b826020860160208301379182016020019490945295945050505050565b600080600080600060a08688031215612e4e578081fd5b85359450612e5e60208701612bb1565b9350612e6c60408701612bb1565b925060608601359150612e8160808701612bb1565b90509295509295909350565b7f416363657373436f6e74726f6c3a206163636f756e7420000000000000000000815260008351612ec5816017850160208801612fcb565b7f206973206d697373696e6720726f6c65200000000000000000000000000000006017918401918201528351612f02816028840160208801612fcb565b01602801949350505050565b6020815260008251806020840152612f2d816040850160208701612fcb565b601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169190910160400192915050565b60008219821115612f7257612f726130bd565b500190565b6000817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0483118215151615612faf57612faf6130bd565b500290565b600082821015612fc657612fc66130bd565b500390565b60005b83811015612fe6578181015183820152602001612fce565b83811115612ff5576000848401525b50505050565b60008161300a5761300a6130bd565b507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0190565b600181811c9082168061304457607f821691505b6020821081141561307e577f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b50919050565b60007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8214156130b6576130b66130bd565b5060010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fdfea264697066735822122086360fa7ed2700fdfaa6056f800b19c6d5f302048602c99f8f1c57c70ff5b08464736f6c63430008040033"); + token_ = DEIStablecoin(token); + + lossless = address(new SymAccount()); + token_.initialize(1_000_000_000e18, address(this), address(this), 0, lossless); + assert(token_.balanceOf(address(this)) == 1_000_000_000e18); + + holders = new address[](3); + holders[0] = address(0x1001); + holders[1] = address(0x1002); + holders[2] = address(0x1003); + + for (uint i = 0; i < holders.length; i++) { + address account = holders[i]; + uint256 balance = svm.createUint256('balance'); + token_.transfer(account, balance); + for (uint j = 0; j < i; j++) { + address other = holders[j]; + uint256 amount = svm.createUint256('amount'); + vm.prank(account); + token_.approve(other, amount); + } + } + } + + function check_NoBackdoor(bytes4 selector, address caller, address other) public { + bytes memory args; + if (selector == token_.acceptRecoveryAdminOwnership.selector) { + args = abi.encode(svm.createBytes(32, 'data')); + } else if (selector == token_.transferOutBlacklistedFunds.selector) { + vm.assume(caller != lossless); // lossless can transfer any + address[] memory from = new address[](1); + from[0] = svm.createAddress('from'); + args = abi.encode(from); + } else { + args = svm.createBytes(1024, 'data'); + } + _checkNoBackdoor(selector, args, caller, other); + } +} diff --git a/examples/tokens/ERC20/test/ERC20Test.sol b/examples/tokens/ERC20/test/ERC20Test.sol new file mode 100644 index 00000000..3b6d0670 --- /dev/null +++ b/examples/tokens/ERC20/test/ERC20Test.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {SymTest} from "halmos-cheatcodes/SymTest.sol"; +import {Test} from "forge-std/Test.sol"; + +import {IERC20} from "forge-std/interfaces/IERC20.sol"; + +abstract contract ERC20Test is SymTest, Test { + // erc20 token address + address internal token; + + // token holders + address[] internal holders; + + function setUp() public virtual; + + function _checkNoBackdoor(bytes4 selector, bytes memory args, address caller, address other) public virtual { + // consider two arbitrary distinct accounts + vm.assume(other != caller); + + // record their current balances + uint256 oldBalanceOther = IERC20(token).balanceOf(other); + + uint256 oldAllowance = IERC20(token).allowance(other, caller); + + // consider an arbitrary function call to the token from the caller + vm.prank(caller); + (bool success,) = address(token).call(abi.encodePacked(selector, args)); + vm.assume(success); + + uint256 newBalanceOther = IERC20(token).balanceOf(other); + + // ensure that the caller cannot spend other' tokens without approvals + if (newBalanceOther < oldBalanceOther) { + assert(oldAllowance >= oldBalanceOther - newBalanceOther); + } + } + + function check_transfer(address sender, address receiver, address other, uint256 amount) public virtual { + // consider other that are neither sender or receiver + require(other != sender); + require(other != receiver); + + // record their current balance + uint256 oldBalanceSender = IERC20(token).balanceOf(sender); + uint256 oldBalanceReceiver = IERC20(token).balanceOf(receiver); + uint256 oldBalanceOther = IERC20(token).balanceOf(other); + + vm.prank(sender); + IERC20(token).transfer(receiver, amount); + + if (sender != receiver) { + assert(IERC20(token).balanceOf(sender) <= oldBalanceSender); // ensure no subtraction overflow + assert(IERC20(token).balanceOf(sender) == oldBalanceSender - amount); + assert(IERC20(token).balanceOf(receiver) >= oldBalanceReceiver); // ensure no addition overflow + assert(IERC20(token).balanceOf(receiver) == oldBalanceReceiver + amount); + } else { + // sender and receiver may be the same + assert(IERC20(token).balanceOf(sender) == oldBalanceSender); + assert(IERC20(token).balanceOf(receiver) == oldBalanceReceiver); + } + // make sure other balance is not affected + assert(IERC20(token).balanceOf(other) == oldBalanceOther); + } + + function check_transferFrom(address caller, address from, address to, address other, uint256 amount) public virtual { + require(other != from); + require(other != to); + + uint256 oldBalanceFrom = IERC20(token).balanceOf(from); + uint256 oldBalanceTo = IERC20(token).balanceOf(to); + uint256 oldBalanceOther = IERC20(token).balanceOf(other); + + uint256 oldAllowance = IERC20(token).allowance(from, caller); + + vm.prank(caller); + IERC20(token).transferFrom(from, to, amount); + + if (from != to) { + assert(IERC20(token).balanceOf(from) <= oldBalanceFrom); + assert(IERC20(token).balanceOf(from) == oldBalanceFrom - amount); + assert(IERC20(token).balanceOf(to) >= oldBalanceTo); + assert(IERC20(token).balanceOf(to) == oldBalanceTo + amount); + + assert(oldAllowance >= amount); // ensure allowance was enough + assert(oldAllowance == type(uint256).max || IERC20(token).allowance(from, caller) == oldAllowance - amount); // allowance decreases if not max + } else { + assert(IERC20(token).balanceOf(from) == oldBalanceFrom); + assert(IERC20(token).balanceOf(to) == oldBalanceTo); + } + assert(IERC20(token).balanceOf(other) == oldBalanceOther); + } +} diff --git a/examples/tokens/ERC20/test/OpenZeppelinERC20.t.sol b/examples/tokens/ERC20/test/OpenZeppelinERC20.t.sol new file mode 100644 index 00000000..d21913e7 --- /dev/null +++ b/examples/tokens/ERC20/test/OpenZeppelinERC20.t.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {ERC20Test} from "./ERC20Test.sol"; + +import {OpenZeppelinERC20} from "../src/OpenZeppelinERC20.sol"; + +/// @custom:halmos --solver-timeout-assertion 0 +contract OpenZeppelinERC20Test is ERC20Test { + + /// @custom:halmos --solver-timeout-branching 1000 + function setUp() public override { + address deployer = address(0x1000); + + OpenZeppelinERC20 token_ = new OpenZeppelinERC20("OpenZeppelinERC20", "OpenZeppelinERC20", 1_000_000_000e18, deployer); + token = address(token_); + + holders = new address[](3); + holders[0] = address(0x1001); + holders[1] = address(0x1002); + holders[2] = address(0x1003); + + for (uint i = 0; i < holders.length; i++) { + address account = holders[i]; + uint256 balance = svm.createUint256('balance'); + vm.prank(deployer); + token_.transfer(account, balance); + for (uint j = 0; j < i; j++) { + address other = holders[j]; + uint256 amount = svm.createUint256('amount'); + vm.prank(account); + token_.approve(other, amount); + } + } + } + + function check_NoBackdoor(bytes4 selector, address caller, address other) public { + bytes memory args = svm.createBytes(1024, 'data'); + _checkNoBackdoor(selector, args, caller, other); + } +} diff --git a/examples/tokens/ERC20/test/SoladyERC20.t.sol b/examples/tokens/ERC20/test/SoladyERC20.t.sol new file mode 100644 index 00000000..2c45da8c --- /dev/null +++ b/examples/tokens/ERC20/test/SoladyERC20.t.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {ERC20Test} from "./ERC20Test.sol"; + +import {SoladyERC20} from "../src/SoladyERC20.sol"; + +/// @custom:halmos --storage-layout=generic --solver-timeout-assertion 0 +contract SoladyERC20Test is ERC20Test { + + /// @custom:halmos --solver-timeout-branching 1000 + function setUp() public override { + address deployer = address(0x1000); + + SoladyERC20 token_ = new SoladyERC20("SoladyERC20", "SoladyERC20", 18, 1_000_000_000e18, deployer); + token = address(token_); + + holders = new address[](3); + holders[0] = address(0x1001); + holders[1] = address(0x1002); + holders[2] = address(0x1003); + + for (uint i = 0; i < holders.length; i++) { + address account = holders[i]; + uint256 balance = svm.createUint256('balance'); + vm.prank(deployer); + token_.transfer(account, balance); + for (uint j = 0; j < i; j++) { + address other = holders[j]; + uint256 amount = svm.createUint256('amount'); + vm.prank(account); + token_.approve(other, amount); + } + } + } + + function check_NoBackdoor(bytes4 selector, address caller, address other) public { + bytes memory args = svm.createBytes(1024, 'data'); + _checkNoBackdoor(selector, args, caller, other); + } +} diff --git a/examples/tokens/ERC20/test/SolmateERC20.t.sol b/examples/tokens/ERC20/test/SolmateERC20.t.sol new file mode 100644 index 00000000..6e64252e --- /dev/null +++ b/examples/tokens/ERC20/test/SolmateERC20.t.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {ERC20Test} from "./ERC20Test.sol"; + +import {SolmateERC20} from "../src/SolmateERC20.sol"; + +/// @custom:halmos --solver-timeout-assertion 0 +contract SolmateERC20Test is ERC20Test { + + /// @custom:halmos --solver-timeout-branching 1000 + function setUp() public override { + address deployer = address(0x1000); + + SolmateERC20 token_ = new SolmateERC20("SolmateERC20", "SolmateERC20", 18, 1_000_000_000e18, deployer); + token = address(token_); + + holders = new address[](3); + holders[0] = address(0x1001); + holders[1] = address(0x1002); + holders[2] = address(0x1003); + + for (uint i = 0; i < holders.length; i++) { + address account = holders[i]; + uint256 balance = svm.createUint256('balance'); + vm.prank(deployer); + token_.transfer(account, balance); + for (uint j = 0; j < i; j++) { + address other = holders[j]; + uint256 amount = svm.createUint256('amount'); + vm.prank(account); + token_.approve(other, amount); + } + } + } + + function check_NoBackdoor(bytes4 selector, address caller, address other) public { + bytes memory args = svm.createBytes(1024, 'data'); + _checkNoBackdoor(selector, args, caller, other); + } +} diff --git a/examples/tokens/ERC721/foundry.toml b/examples/tokens/ERC721/foundry.toml new file mode 100644 index 00000000..d85bbc96 --- /dev/null +++ b/examples/tokens/ERC721/foundry.toml @@ -0,0 +1,4 @@ +[profile.default] +src = "src" +out = "out" +libs = ["../../../tests/lib", "lib"] diff --git a/examples/tokens/ERC721/remappings.txt b/examples/tokens/ERC721/remappings.txt new file mode 100644 index 00000000..acf3e02d --- /dev/null +++ b/examples/tokens/ERC721/remappings.txt @@ -0,0 +1,2 @@ +openzeppelin/=../../../tests/lib/openzeppelin-contracts/contracts/ +ds-test/=../../../tests/lib/forge-std/lib/ds-test/src/ diff --git a/examples/tokens/ERC721/src/OpenZeppelinERC721.sol b/examples/tokens/ERC721/src/OpenZeppelinERC721.sol new file mode 100644 index 00000000..5935e84b --- /dev/null +++ b/examples/tokens/ERC721/src/OpenZeppelinERC721.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {ERC721} from "openzeppelin/token/ERC721/ERC721.sol"; + +contract OpenZeppelinERC721 is ERC721 { + constructor(string memory name, string memory symbol, uint256 initialSupply, address deployer) ERC721(name, symbol) { + for (uint256 i = 1; i <= initialSupply; i++) { + _mint(deployer, i); + } + } +} diff --git a/examples/tokens/ERC721/src/SoladyERC721.sol b/examples/tokens/ERC721/src/SoladyERC721.sol new file mode 100644 index 00000000..2ec6869a --- /dev/null +++ b/examples/tokens/ERC721/src/SoladyERC721.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {ERC721} from "solady/tokens/ERC721.sol"; + +contract SoladyERC721 is ERC721 { + string internal _name; + string internal _symbol; + + constructor( + string memory name_, + string memory symbol_, + uint256 initialSupply, + address deployer + ) { + _name = name_; + _symbol = symbol_; + + for (uint256 i = 1; i <= initialSupply; i++) { + _mint(deployer, i); + } + } + + function name() public view virtual override returns (string memory) { + return _name; + } + + function symbol() public view virtual override returns (string memory) { + return _symbol; + } + + function tokenURI(uint256) public view virtual override returns (string memory) {} +} diff --git a/examples/tokens/ERC721/src/SolmateERC721.sol b/examples/tokens/ERC721/src/SolmateERC721.sol new file mode 100644 index 00000000..cc1ef2ee --- /dev/null +++ b/examples/tokens/ERC721/src/SolmateERC721.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {ERC721} from "solmate/tokens/ERC721.sol"; + +contract SolmateERC721 is ERC721 { + constructor(string memory name, string memory symbol, uint256 initialSupply, address deployer) ERC721(name, symbol) { + for (uint256 i = 1; i <= initialSupply; i++) { + _mint(deployer, i); + } + } + + function tokenURI(uint256) public pure virtual override returns (string memory) {} +} diff --git a/examples/tokens/ERC721/test/ERC721Test.sol b/examples/tokens/ERC721/test/ERC721Test.sol new file mode 100644 index 00000000..83f77f20 --- /dev/null +++ b/examples/tokens/ERC721/test/ERC721Test.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {SymTest} from "halmos-cheatcodes/SymTest.sol"; +import {Test} from "forge-std/Test.sol"; + +import {IERC721} from "forge-std/interfaces/IERC721.sol"; + +abstract contract ERC721Test is SymTest, Test { + address internal token; + address internal deployer; + address[] internal accounts; + uint256[] internal tokenIds; + + function setUp() public virtual; + + function check_NoBackdoor(bytes4 selector) public virtual { + // consider caller and other that are distinct + address caller = svm.createAddress('caller'); + address other = svm.createAddress('other'); + vm.assume(caller != other); + + // assume the caller hasn't been granted any approvals + for (uint i = 0; i < accounts.length; i++) { + vm.assume(!IERC721(token).isApprovedForAll(accounts[i], caller)); + } + for (uint i = 0; i < tokenIds.length; i++) { + vm.assume(IERC721(token).getApproved(tokenIds[i]) != caller); + } + + // record their current balances + uint256 oldBalanceCaller = IERC721(token).balanceOf(caller); + uint256 oldBalanceOther = IERC721(token).balanceOf(other); + + // consider an arbitrary function call to the token from the caller + vm.prank(caller); + bool success; + if (uint32(selector) == 0xb88d4fde) { // TODO: support parameters of type bytes or dynamic arrays + (success,) = address(token).call(abi.encodeWithSelector(selector, svm.createAddress('from'), svm.createAddress('to'), svm.createUint256('tokenId'), svm.createBytes(96, 'data'))); + } else { + bytes memory args = svm.createBytes(1024, 'args'); + (success,) = address(token).call(abi.encodePacked(selector, args)); + } + vm.assume(success); + + // ensure that the caller cannot spend other's tokens + assert(IERC721(token).balanceOf(caller) <= oldBalanceCaller); + assert(IERC721(token).balanceOf(other) >= oldBalanceOther); + } + + function _isApprovedOrOwner(address spender, uint256 tokenId) internal view returns (bool) { + address owner = IERC721(token).ownerOf(tokenId); + return (spender == owner || IERC721(token).isApprovedForAll(owner, spender) || IERC721(token).getApproved(tokenId) == spender); + } + + function check_transferFrom(address caller, address from, address to, address other, uint256 tokenId, uint256 otherTokenId) public virtual { + // consider other address + require(other != from); + require(other != to); + + // consider other token ids + require(otherTokenId != tokenId); + + // record their current balance + uint256 oldBalanceFrom = IERC721(token).balanceOf(from); + uint256 oldBalanceTo = IERC721(token).balanceOf(to); + uint256 oldBalanceOther = IERC721(token).balanceOf(other); + + // record their current owner + address oldOwner = IERC721(token).ownerOf(tokenId); + address oldOtherTokenOwner = IERC721(token).ownerOf(otherTokenId); + + // record the current approvals + bool approved = _isApprovedOrOwner(caller, tokenId); + + vm.prank(caller); + if (svm.createBool('?')) { + IERC721(token).transferFrom(from, to, tokenId); + } else { + IERC721(token).safeTransferFrom(from, to, tokenId, svm.createBytes(96, 'data')); + } + + // ensure requirements of transfer + assert(from == oldOwner); + assert(approved); + + // ensure the owner is updated correctly + assert(IERC721(token).ownerOf(tokenId) == to); + assert(IERC721(token).getApproved(tokenId) == address(0)); // ensure the approval is reset + + // ensure the other token's owner is unchanged + assert(IERC721(token).ownerOf(otherTokenId) == oldOtherTokenOwner); + + // balance update + if (from != to) { + assert(IERC721(token).balanceOf(from) < oldBalanceFrom); + assert(IERC721(token).balanceOf(from) == oldBalanceFrom - 1); + assert(IERC721(token).balanceOf(to) > oldBalanceTo); + assert(IERC721(token).balanceOf(to) == oldBalanceTo + 1); + } else { + assert(IERC721(token).balanceOf(from) == oldBalanceFrom); + assert(IERC721(token).balanceOf(to) == oldBalanceTo); + } + assert(IERC721(token).balanceOf(other) == oldBalanceOther); + } +} diff --git a/examples/tokens/ERC721/test/OpenZeppelinERC721.t.sol b/examples/tokens/ERC721/test/OpenZeppelinERC721.t.sol new file mode 100644 index 00000000..d661dc37 --- /dev/null +++ b/examples/tokens/ERC721/test/OpenZeppelinERC721.t.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {ERC721Test} from "./ERC721Test.sol"; + +import {OpenZeppelinERC721} from "../src/OpenZeppelinERC721.sol"; + +/// @custom:halmos --solver-timeout-assertion 0 +contract OpenZeppelinERC721Test is ERC721Test { + + /// @custom:halmos --solver-timeout-branching 1000 + function setUp() public override { + deployer = address(0x1000); + + OpenZeppelinERC721 token_ = new OpenZeppelinERC721("OpenZeppelinERC721", "OpenZeppelinERC721", 5, deployer); + token = address(token_); + + accounts = new address[](3); + accounts[0] = address(0x1001); + accounts[1] = address(0x1002); + accounts[2] = address(0x1003); + + tokenIds = new uint256[](5); + tokenIds[0] = 1; + tokenIds[1] = 2; + tokenIds[2] = 3; + tokenIds[3] = 4; + tokenIds[4] = 5; + + // account0: {token0, token1}, account1: {token2}, account2: {token3} + vm.prank(deployer); + token_.transferFrom(deployer, accounts[0], tokenIds[0]); + vm.prank(deployer); + token_.transferFrom(deployer, accounts[0], tokenIds[1]); + vm.prank(deployer); + token_.transferFrom(deployer, accounts[1], tokenIds[2]); + vm.prank(deployer); + token_.transferFrom(deployer, accounts[2], tokenIds[3]); + + vm.prank(accounts[0]); + token_.approve(accounts[2], tokenIds[0]); + + vm.prank(accounts[1]); + token_.setApprovalForAll(accounts[2], true); + } +} diff --git a/examples/tokens/ERC721/test/SoladyERC721.t.sol b/examples/tokens/ERC721/test/SoladyERC721.t.sol new file mode 100644 index 00000000..eaa36693 --- /dev/null +++ b/examples/tokens/ERC721/test/SoladyERC721.t.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {ERC721Test} from "./ERC721Test.sol"; + +import {SoladyERC721} from "../src/SoladyERC721.sol"; + +/// @custom:halmos --storage-layout=generic --solver-timeout-assertion 0 +contract SoladyERC721Test is ERC721Test { + + /// @custom:halmos --solver-timeout-branching 1000 + function setUp() public override { + deployer = address(0x1000); + + SoladyERC721 token_ = new SoladyERC721("SoladyERC721", "SoladyERC721", 5, deployer); + token = address(token_); + + accounts = new address[](3); + accounts[0] = address(0x1001); + accounts[1] = address(0x1002); + accounts[2] = address(0x1003); + + tokenIds = new uint256[](5); + tokenIds[0] = 1; + tokenIds[1] = 2; + tokenIds[2] = 3; + tokenIds[3] = 4; + tokenIds[4] = 5; + + // account0: {token0, token1}, account1: {token2}, account2: {token3} + vm.prank(deployer); + token_.transferFrom(deployer, accounts[0], tokenIds[0]); + vm.prank(deployer); + token_.transferFrom(deployer, accounts[0], tokenIds[1]); + vm.prank(deployer); + token_.transferFrom(deployer, accounts[1], tokenIds[2]); + vm.prank(deployer); + token_.transferFrom(deployer, accounts[2], tokenIds[3]); + + vm.prank(accounts[0]); + token_.approve(accounts[2], tokenIds[0]); + + vm.prank(accounts[1]); + token_.setApprovalForAll(accounts[2], true); + } +} diff --git a/examples/tokens/ERC721/test/SolmateERC721.t.sol b/examples/tokens/ERC721/test/SolmateERC721.t.sol new file mode 100644 index 00000000..579ed92f --- /dev/null +++ b/examples/tokens/ERC721/test/SolmateERC721.t.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {ERC721Test} from "./ERC721Test.sol"; + +import {SolmateERC721} from "../src/SolmateERC721.sol"; + +/// @custom:halmos --solver-timeout-assertion 0 +contract SolmateERC721Test is ERC721Test { + + /// @custom:halmos --solver-timeout-branching 1000 + function setUp() public override { + deployer = address(0x1000); + + SolmateERC721 token_ = new SolmateERC721("SolmateERC721", "SolmateERC721", 5, deployer); + token = address(token_); + + accounts = new address[](3); + accounts[0] = address(0x1001); + accounts[1] = address(0x1002); + accounts[2] = address(0x1003); + + tokenIds = new uint256[](5); + tokenIds[0] = 1; + tokenIds[1] = 2; + tokenIds[2] = 3; + tokenIds[3] = 4; + tokenIds[4] = 5; + + // account0: {token0, token1}, account1: {token2}, account2: {token3} + vm.prank(deployer); + token_.transferFrom(deployer, accounts[0], tokenIds[0]); + vm.prank(deployer); + token_.transferFrom(deployer, accounts[0], tokenIds[1]); + vm.prank(deployer); + token_.transferFrom(deployer, accounts[1], tokenIds[2]); + vm.prank(deployer); + token_.transferFrom(deployer, accounts[2], tokenIds[3]); + + vm.prank(accounts[0]); + token_.approve(accounts[2], tokenIds[0]); + + vm.prank(accounts[1]); + token_.setApprovalForAll(accounts[2], true); + } +} diff --git a/pyproject.toml b/pyproject.toml index 056b2e05..8e365444 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,19 +6,23 @@ build-backend = "setuptools.build_meta" [project] name = "halmos" -description = "Halmos: Symbolic Bounded Model Checker for Ethereum Smart Contracts Bytecode" +description = "A symbolic testing tool for EVM smart contracts" readme = "README.md" authors = [ + { name="a16z crypto" }, +] +maintainers = [ { name="Daejun Park" }, + { name="karmacoma " }, ] classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: GNU Affero General Public License v3", "Operating System :: OS Independent", ] -requires-python = ">=3.8" +requires-python = ">=3.9" dependencies = [ - "z3-solver", + "z3-solver==4.12.2.0", ] dynamic = ["version"] @@ -29,4 +33,8 @@ halmos = "halmos.__main__:main" "Homepage" = "https://github.com/a16z/halmos" [tool.black] -target-versions = ["py38", "py39", "py310", "py311"] +target-versions = ["py39", "py310", "py311"] + +[tool.pytest.ini_options] +# TODO: re-add test_traces.py when we have a better way to support it in CI +addopts = "--ignore=tests/lib --ignore=tests/test_traces.py" diff --git a/requirements.txt b/requirements.txt index c91c87dd..d8637d4e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -z3-solver +z3-solver==4.12.2.0 diff --git a/src/halmos/__main__.py b/src/halmos/__main__.py index a511e9cd..ed5b3155 100644 --- a/src/halmos/__main__.py +++ b/src/halmos/__main__.py @@ -5,223 +5,65 @@ import subprocess import uuid import json -import argparse import re +import signal import traceback +import time -from dataclasses import dataclass -from timeit import default_timer as timer +from argparse import Namespace +from dataclasses import dataclass, asdict from importlib import metadata +from enum import Enum +from collections import Counter + +from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor -from .pools import thread_pool, process_pool from .sevm import * -from .utils import color_good, color_warn, hexify +from .utils import ( + create_solver, + hexify, + stringify, + indent_text, + NamedTimer, + yellow, + cyan, + green, + red, + error, + info, + color_good, + color_warn, + color_info, + color_error, +) from .warnings import * +from .parser import mk_arg_parser +from .calldata import Calldata -SETUP_FAILED = 127 -TEST_FAILED = 128 - -args: argparse.Namespace +StrModel = Dict[str, str] +AnyModel = UnionType[Model, StrModel] +arg_parser = mk_arg_parser() # Python version >=3.8.14, >=3.9.14, >=3.10.7, or >=3.11 if hasattr(sys, "set_int_max_str_digits"): sys.set_int_max_str_digits(0) +# we need to be able to process at least the max message depth (1024) +sys.setrecursionlimit(1024 * 4) -def parse_args(args=None) -> argparse.Namespace: - parser = argparse.ArgumentParser( - prog="halmos", epilog="For more information, see https://github.com/a16z/halmos" - ) - - parser.add_argument( - "--root", - metavar="DIRECTORY", - default=os.getcwd(), - help="source root directory (default: current directory)", - ) - parser.add_argument( - "--contract", - metavar="CONTRACT_NAME", - help="run tests in the given contract only", - ) - parser.add_argument( - "--function", - metavar="FUNCTION_NAME_PREFIX", - default="check", - help="run tests matching the given prefix only (default: %(default)s)", - ) - - parser.add_argument( - "--loop", - metavar="MAX_BOUND", - type=int, - default=2, - help="set loop unrolling bounds (default: %(default)s)", - ) - parser.add_argument( - "--width", metavar="MAX_WIDTH", type=int, help="set the max number of paths" - ) - parser.add_argument( - "--depth", metavar="MAX_DEPTH", type=int, help="set the max path length" - ) - parser.add_argument( - "--array-lengths", - metavar="NAME1=LENGTH1,NAME2=LENGTH2,...", - help="set the length of dynamic-sized arrays including bytes and string (default: loop unrolling bound)", - ) - - parser.add_argument( - "--symbolic-storage", - action="store_true", - help="set default storage values to symbolic", - ) - parser.add_argument( - "--symbolic-msg-sender", action="store_true", help="set msg.sender symbolic" - ) +# sometimes defaults to cp1252 on Windows, which can cause UnicodeEncodeError +if sys.stdout.encoding != "utf-8": + sys.stdout.reconfigure(encoding="utf-8") - parser.add_argument( - "--version", action="store_true", help="print the version number" - ) +# Panic(1) +# bytes4(keccak256("Panic(uint256)")) + bytes32(1) +ASSERT_FAIL = 0x4E487B710000000000000000000000000000000000000000000000000000000000000001 - # debugging options - group_debug = parser.add_argument_group("Debugging options") - - group_debug.add_argument( - "-v", - "--verbose", - action="count", - default=0, - help="increase verbosity levels: -v, -vv, -vvv, ...", - ) - group_debug.add_argument( - "-st", "--statistics", action="store_true", help="print statistics" - ) - group_debug.add_argument("--debug", action="store_true", help="run in debug mode") - group_debug.add_argument( - "--log", metavar="LOG_FILE_PATH", help="log every execution steps in JSON" - ) - group_debug.add_argument( - "--print-steps", action="store_true", help="print every execution steps" - ) - group_debug.add_argument( - "--print-states", action="store_true", help="print all final execution states" - ) - group_debug.add_argument( - "--print-failed-states", - action="store_true", - help="print failed execution states", - ) - group_debug.add_argument( - "--print-blocked-states", - action="store_true", - help="print blocked execution states", - ) - group_debug.add_argument( - "--print-setup-states", action="store_true", help="print setup execution states" - ) - group_debug.add_argument( - "--print-full-model", - action="store_true", - help="print full counterexample model", - ) - - # build options - group_build = parser.add_argument_group("Build options") - - group_build.add_argument( - "--forge-build-out", - metavar="DIRECTORY_NAME", - default="out", - help="forge build artifacts directory name (default: %(default)s)", - ) - - # smt solver options - group_solver = parser.add_argument_group("Solver options") - - group_solver.add_argument( - "--no-smt-add", action="store_true", help="do not interpret `+`" - ) - group_solver.add_argument( - "--no-smt-sub", action="store_true", help="do not interpret `-`" - ) - group_solver.add_argument( - "--no-smt-mul", action="store_true", help="do not interpret `*`" - ) - group_solver.add_argument("--smt-div", action="store_true", help="interpret `/`") - group_solver.add_argument("--smt-mod", action="store_true", help="interpret `mod`") - group_solver.add_argument( - "--smt-div-by-const", action="store_true", help="interpret division by constant" - ) - group_solver.add_argument( - "--smt-mod-by-const", action="store_true", help="interpret constant modulo" - ) - group_solver.add_argument( - "--smt-exp-by-const", - metavar="N", - type=int, - default=2, - help="interpret constant power up to N (default: %(default)s)", - ) - - group_solver.add_argument( - "--solver-timeout-branching", - metavar="TIMEOUT", - type=int, - default=1, - help="set timeout (in milliseconds) for solving branching conditions (default: %(default)s)", - ) - group_solver.add_argument( - "--solver-timeout-assertion", - metavar="TIMEOUT", - type=int, - default=1000, - help="set timeout (in milliseconds) for solving assertion violation conditions (default: %(default)s)", - ) - group_solver.add_argument( - "--solver-fresh", - action="store_true", - help="run an extra solver with a fresh state for unknown", - ) - group_solver.add_argument( - "--solver-subprocess", - action="store_true", - help="run an extra solver in subprocess for unknown", - ) - group_solver.add_argument( - "--solver-parallel", - action="store_true", - help="run assertion solvers in parallel", - ) - - # internal options - group_internal = parser.add_argument_group("Internal options") - - group_internal.add_argument( - "--bytecode", metavar="HEX_STRING", help="execute the given bytecode" - ) - group_internal.add_argument( - "--reset-bytecode", - metavar="ADDR1=CODE1,ADDR2=CODE2,...", - help="reset the bytecode of given addresses after setUp()", - ) - group_internal.add_argument( - "--test-parallel", action="store_true", help="run tests in parallel" - ) - - # experimental options - group_experimental = parser.add_argument_group("Experimental options") - - group_experimental.add_argument( - "--symbolic-jump", action="store_true", help="support symbolic jump destination" - ) - group_experimental.add_argument( - "--print-potential-counterexample", - action="store_true", - help="print potentially invalid counterexamples", - ) - - return parser.parse_args(args) +VERBOSITY_TRACE_COUNTEREXAMPLE = 2 +VERBOSITY_TRACE_SETUP = 3 +VERBOSITY_TRACE_PATHS = 4 +VERBOSITY_TRACE_CONSTRUCTOR = 5 @dataclass(frozen=True) @@ -236,8 +78,9 @@ def str_tuple(args: List) -> str: ret = [] for arg in args: typ = arg["type"] - if typ == "tuple": - ret.append(str_tuple(arg["components"])) + match = re.search(r"^tuple((\[[0-9]*\])*)$", typ) + if match: + ret.append(str_tuple(arg["components"]) + match.group(1)) else: ret.append(typ) return "(" + ",".join(ret) + ")" @@ -264,76 +107,21 @@ def mk_calldata( fun_info: FunctionInfo, cd: List, dyn_param_size: List[str], - args: argparse.Namespace, + args: Namespace, ) -> None: - item = find_abi(abi, fun_info) - tba = [] - offset = 0 - for param in item["inputs"]: - param_name = param["name"] - param_type = param["type"] - if param_type == "tuple": - # TODO: support struct types - raise NotImplementedError(f"Not supported parameter type: {param_type}") - elif param_type == "bytes" or param_type == "string": - # wstore(cd, 4+offset, 32, BitVecVal(, 256)) - tba.append((4 + offset, param)) - offset += 32 - elif param_type.endswith("[]"): - raise NotImplementedError(f"Not supported dynamic arrays: {param_type}") - else: - match = re.search( - r"(u?int[0-9]*|address|bool|bytes[0-9]+)(\[([0-9]+)\])?", param_type - ) - if not match: - raise NotImplementedError(f"Unknown parameter type: {param_type}") - typ = match.group(1) - dim = match.group(3) - if dim: # array - for idx in range(int(dim)): - wstore( - cd, 4 + offset, 32, BitVec(f"p_{param_name}[{idx}]_{typ}", 256) - ) - offset += 32 - else: # primitive - wstore(cd, 4 + offset, 32, BitVec(f"p_{param_name}_{typ}", 256)) - offset += 32 - - arrlen = mk_arrlen(args) - for loc_param in tba: - loc = loc_param[0] - param = loc_param[1] - param_name = param["name"] - param_type = param["type"] - - if param_name not in arrlen: - size = args.loop - if args.debug: - print( - f"Warning: no size provided for {param_name}; default value {size} will be used." - ) - else: - size = arrlen[param_name] - - dyn_param_size.append(f"|{param_name}|={size}") - - if param_type == "bytes" or param_type == "string": - # head - wstore(cd, loc, 32, BitVecVal(offset, 256)) - # tail - size_pad_right = int((size + 31) / 32) * 32 - wstore(cd, 4 + offset, 32, BitVecVal(size, 256)) - offset += 32 - if size_pad_right > 0: - wstore( - cd, - 4 + offset, - size_pad_right, - BitVec(f"p_{param_name}_{param_type}", 8 * size_pad_right), - ) - offset += size_pad_right - else: - raise ValueError(param_type) + # find function abi + fun_abi = find_abi(abi, fun_info) + + # no parameters + if len(fun_abi["inputs"]) == 0: + return + + # generate symbolic ABI calldata + calldata = Calldata(args, mk_arrlen(args), dyn_param_size) + result = calldata.create(fun_abi) + + # TODO: use Contract abstraction for calldata + wstore(cd, 4, result.size() // 8, result) def mk_callvalue() -> Word: @@ -362,7 +150,7 @@ def mk_addr(name: str) -> Address: return BitVec(name, 160) -def mk_caller(args: argparse.Namespace) -> Address: +def mk_caller(args: Namespace) -> Address: if args.symbolic_msg_sender: return mk_addr("msg_sender") else: @@ -373,133 +161,353 @@ def mk_this() -> Address: return con_addr(magic_address + 1) -def mk_solver(args: argparse.Namespace): - # quantifier-free bitvector + array theory; https://smtlib.cs.uiowa.edu/logics.shtml - solver = SolverFor("QF_AUFBV") - solver.set(timeout=args.solver_timeout_branching) - return solver +def mk_solver(args: Namespace, logic="QF_AUFBV", ctx=None, assertion=False): + timeout = ( + args.solver_timeout_assertion if assertion else args.solver_timeout_branching + ) + return create_solver(logic, ctx, timeout, args.solver_max_memory) -def run_bytecode(hexcode: str) -> List[Exec]: - contract = Contract.from_hexcode(hexcode) +def rendered_initcode(context: CallContext) -> str: + message = context.message + data = message.data - storage = {} + initcode_str = "" + args_str = "" - solver = mk_solver(args) + if ( + isinstance(data, BitVecRef) + and is_app(data) + and data.decl().kind() == Z3_OP_CONCAT + ): + children = [arg for arg in data.children()] + if isinstance(children[0], BitVecNumRef): + initcode_str = hex(children[0].as_long()) + args_str = ", ".join(map(str, children[1:])) + else: + initcode_str = hexify(data) + + return f"{initcode_str}({color_info(args_str)})" + + +def render_output(context: CallContext, file=sys.stdout) -> None: + output = context.output + returndata_str = "0x" + failed = output.error is not None + + if not failed and context.is_stuck(): + return + + if output.data is not None: + is_create = context.message.is_create() + + returndata_str = ( + f"<{byte_length(output.data)} bytes of code>" + if (is_create and not failed) + else hexify(output.data) + ) + + ret_scheme = context.output.return_scheme + ret_scheme_str = f"{cyan(mnemonic(ret_scheme))} " if ret_scheme is not None else "" + error_str = f" (error: {repr(output.error)})" if failed else "" + + color = red if failed else green + indent = context.depth * " " + print( + f"{indent}{color('↩ ')}{ret_scheme_str}{color(returndata_str)}{color(error_str)}", + file=file, + ) + + +def rendered_log(log: EventLog) -> str: + opcode_str = f"LOG{len(log.topics)}" + topics = [ + f"{color_info(f'topic{i}')}={hexify(topic)}" + for i, topic in enumerate(log.topics) + ] + data_str = f"{color_info('data')}={hexify(log.data)}" + args_str = ", ".join(topics + [data_str]) + + return f"{opcode_str}({args_str})" + + +def rendered_trace(context: CallContext) -> str: + with io.StringIO() as output: + render_trace(context, file=output) + return output.getvalue() + + +def rendered_calldata(calldata: List[Byte]) -> str: + if any(is_bv(x) for x in calldata): + # make sure every byte is wrapped + calldata_bv = [x if is_bv(x) else con(x, 8) for x in calldata] + return hexify(simplify(concat(calldata_bv))) + + return "0x" + bytes(calldata).hex() if calldata else "0x" + + +def render_trace(context: CallContext, file=sys.stdout) -> None: + # TODO: label for known addresses + # TODO: decode calldata + # TODO: decode logs + + message = context.message + addr = unbox_int(message.target) + addr_str = str(addr) if is_bv(addr) else hex(addr) + + value = unbox_int(message.value) + value_str = f" (value: {value})" if is_bv(value) or value > 0 else "" + + call_scheme_str = f"{cyan(mnemonic(message.call_scheme))} " + indent = context.depth * " " + + if message.is_create(): + # TODO: select verbosity level to render full initcode + # initcode_str = rendered_initcode(context) + initcode_str = f"<{byte_length(message.data)} bytes of initcode>" + print( + f"{indent}{call_scheme_str}{addr_str}::{initcode_str}{value_str}", file=file + ) + + else: + calldata = rendered_calldata(message.data) + call_str = f"{addr_str}::{calldata}" + static_str = yellow(" [static]") if message.is_static else "" + print(f"{indent}{call_scheme_str}{call_str}{static_str}{value_str}", file=file) + + log_indent = (context.depth + 1) * " " + for trace_element in context.trace: + if isinstance(trace_element, CallContext): + render_trace(trace_element, file=file) + elif isinstance(trace_element, EventLog): + print(f"{log_indent}{rendered_log(trace_element)}", file=file) + else: + raise HalmosException(f"unexpected trace element: {trace_element}") + render_output(context, file=file) + + if context.depth == 1: + print(file=file) + + +def run_bytecode(hexcode: str, args: Namespace) -> List[Exec]: + solver = mk_solver(args) + contract = Contract.from_hexcode(hexcode) balance = mk_balance() block = mk_block() - callvalue = mk_callvalue() - caller = mk_caller(args) - this = mk_this() options = mk_options(args) + this = mk_this() + + message = Message( + target=this, + caller=mk_caller(args), + value=mk_callvalue(), + data=[], + ) sevm = SEVM(options) ex = sevm.mk_exec( code={this: contract}, - storage={this: storage}, + storage={this: {}}, balance=balance, block=block, - calldata=[], - callvalue=callvalue, - caller=caller, + context=CallContext(message=message), this=this, + pgm=contract, symbolic=args.symbolic_storage, - solver=solver, + path=Path(solver), ) - (exs, _, _) = sevm.run(ex) + exs = sevm.run(ex) + result_exs = [] for idx, ex in enumerate(exs): + result_exs.append(ex) + opcode = ex.current_opcode() - if opcode in [EVM.STOP, EVM.RETURN, EVM.REVERT, EVM.INVALID]: - model_with_context = gen_model(args, idx, ex) - print( - f"Final opcode: {mnemonic(opcode)} | Return data: {ex.output} | Input example: {model_with_context.model}" + error = ex.context.output.error + returndata = ex.context.output.data + + if error: + warn( + INTERNAL_ERROR, + f"{mnemonic(opcode)} failed, error={error}, returndata={returndata}", ) else: - print(color_warn(f"Not supported: {mnemonic(opcode)} {ex.error}")) + print(f"Final opcode: {mnemonic(opcode)})") + print(f"Return data: {returndata}") + dump_dirname = f"/tmp/halmos-{uuid.uuid4().hex}" + model_with_context = gen_model_from_sexpr( + GenModelArgs(args, idx, ex.path.solver.to_smt2(), dump_dirname) + ) + print(f"Input example: {model_with_context.model}") + if args.print_states: - print(f"# {idx+1} / {len(exs)}") + print(f"# {idx+1}") print(ex) - return exs + return result_exs -def setup( - hexcode: str, - abi: List, - setup_info: FunctionInfo, - args: argparse.Namespace, +def deploy_test( + creation_hexcode: str, + deployed_hexcode: str, + sevm: SEVM, + args: Namespace, + libs: Dict, ) -> Exec: - setup_start = timer() - - contract = Contract.from_hexcode(hexcode) - - solver = mk_solver(args) this = mk_this() - options = mk_options(args) - - sevm = SEVM(options) + message = Message( + target=this, + caller=mk_caller(args), + value=con(0), + data=[], + ) - setup_ex = sevm.mk_exec( - code={this: contract}, + ex = sevm.mk_exec( + code={this: Contract(b"")}, storage={this: {}}, balance=mk_balance(), block=mk_block(), - calldata=[], - callvalue=con(0), - caller=mk_caller(args), + context=CallContext(message=message), this=this, + pgm=None, # to be added symbolic=False, - solver=solver, + path=Path(mk_solver(args)), ) - setup_mid = timer() - - setup_sig, setup_name, setup_selector = ( - setup_info.sig, - setup_info.name, - setup_info.selector, + # deploy libraries and resolve library placeholders in hexcode + (creation_hexcode, deployed_hexcode) = ex.resolve_libs( + creation_hexcode, deployed_hexcode, libs ) + + # test contract creation bytecode + creation_bytecode = Contract.from_hexcode(creation_hexcode) + ex.pgm = creation_bytecode + + # use the given deployed bytecode if --no-test-constructor is enabled + if args.no_test_constructor: + deployed_bytecode = Contract.from_hexcode(deployed_hexcode) + ex.code[this] = deployed_bytecode + ex.pgm = deployed_bytecode + return ex + + # create test contract + exs = list(sevm.run(ex)) + + # sanity check + if len(exs) != 1: + raise ValueError(f"constructor: # of paths: {len(exs)}") + + ex = exs[0] + + if args.verbose >= VERBOSITY_TRACE_CONSTRUCTOR: + print("Constructor trace:") + render_trace(ex.context) + + error = ex.context.output.error + returndata = ex.context.output.data + if error: + raise ValueError(f"constructor failed, error={error} returndata={returndata}") + + # deployed bytecode + deployed_bytecode = Contract(returndata) + ex.code[this] = deployed_bytecode + ex.pgm = deployed_bytecode + + # reset vm state + ex.pc = 0 + ex.st = State() + ex.context.output = CallOutput() + ex.jumpis = {} + ex.prank = Prank() + + return ex + + +def setup( + creation_hexcode: str, + deployed_hexcode: str, + abi: List, + setup_info: FunctionInfo, + args: Namespace, + libs: Dict, +) -> Exec: + setup_timer = NamedTimer("setup") + setup_timer.create_subtimer("decode") + + sevm = SEVM(mk_options(args)) + setup_ex = deploy_test(creation_hexcode, deployed_hexcode, sevm, args, libs) + + setup_timer.create_subtimer("run") + + setup_sig, setup_selector = (setup_info.sig, setup_info.selector) if setup_sig: - wstore(setup_ex.calldata, 0, 4, BitVecVal(int(setup_selector, 16), 32)) + calldata = [] + wstore(calldata, 0, 4, BitVecVal(int(setup_selector, 16), 32)) dyn_param_size = [] # TODO: propagate to run - mk_calldata(abi, setup_info, setup_ex.calldata, dyn_param_size, args) + mk_calldata(abi, setup_info, calldata, dyn_param_size, args) + + setup_ex.context = CallContext( + message=Message( + target=setup_ex.message().target, + caller=setup_ex.message().caller, + value=con(0), + data=calldata, + ), + ) - (setup_exs_all, setup_steps, setup_bounded_loops) = sevm.run(setup_ex) + setup_exs_all = sevm.run(setup_ex) - if setup_bounded_loops: - warn( - LOOP_BOUND, - f"{setup_sig}: paths have not been fully explored due to the loop unrolling bound: {args.loop}", - ) - if args.debug: - print("\n".join(setup_bounded_loops)) - - setup_exs = [] + setup_exs_no_error = [] for idx, setup_ex in enumerate(setup_exs_all): + if args.verbose >= VERBOSITY_TRACE_SETUP: + print(f"{setup_sig} trace #{idx+1}:") + render_trace(setup_ex.context) + opcode = setup_ex.current_opcode() - if opcode in [EVM.STOP, EVM.RETURN]: - setup_ex.solver.set(timeout=args.solver_timeout_assertion) - res = setup_ex.solver.check() + error = setup_ex.context.output.error + + if error is None: + setup_exs_no_error.append((setup_ex, setup_ex.path.solver.to_smt2())) + + else: + if opcode not in [EVM.REVERT, EVM.INVALID]: + warn( + INTERNAL_ERROR, + f"Warning: {setup_sig} execution encountered an issue at {mnemonic(opcode)}: {error}", + ) + + # only render the trace if we didn't already do it + if ( + args.verbose < VERBOSITY_TRACE_SETUP + and args.verbose >= VERBOSITY_TRACE_COUNTEREXAMPLE + ): + print(f"{setup_sig} trace:") + render_trace(setup_ex.context) + + setup_exs = [] + + if len(setup_exs_no_error) > 1: + for setup_ex, query in setup_exs_no_error: + res, _ = solve(query, args) if res != unsat: setup_exs.append(setup_ex) - elif opcode not in [EVM.REVERT, EVM.INVALID]: - print( - color_warn( - f"Warning: {setup_sig} execution encountered an issue at {mnemonic(opcode)}: {setup_ex.error}" - ) - ) + if len(setup_exs) > 1: + break + + elif len(setup_exs_no_error) == 1: + setup_exs.append(setup_exs_no_error[0][0]) if len(setup_exs) == 0: - raise ValueError(f"No successful path found in {setup_sig}") + raise HalmosException(f"No successful path found in {setup_sig}") + if len(setup_exs) > 1: - print( - color_warn( - f"Warning: multiple paths were found in {setup_sig}; an arbitrary path has been selected for the following tests." - ) + info( + f"Warning: multiple paths were found in {setup_sig}; " + "an arbitrary path has been selected for the following tests." ) + if args.debug: print("\n".join(map(str, setup_exs))) @@ -508,71 +516,104 @@ def setup( if args.print_setup_states: print(setup_ex) + if sevm.logs.bounded_loops: + warn( + LOOP_BOUND, + f"{setup_sig}: paths have not been fully explored due to the loop unrolling bound: {args.loop}", + ) + if args.debug: + print("\n".join(sevm.logs.bounded_loops)) + if args.reset_bytecode: for assign in [x.split("=") for x in args.reset_bytecode.split(",")]: addr = con_addr(int(assign[0].strip(), 0)) new_hexcode = assign[1].strip() setup_ex.code[addr] = Contract.from_hexcode(new_hexcode) - setup_end = timer() - if args.statistics: - print( - f"[time] setup: {setup_end - setup_start:0.2f}s (decode: {setup_mid - setup_start:0.2f}s, run: {setup_end - setup_mid:0.2f}s)" - ) + print(setup_timer.report()) return setup_ex @dataclass(frozen=True) class ModelWithContext: - model: str - validity: bool + # can be a filename containing the model or a dict with variable assignments + model: Optional[UnionType[StrModel, str]] + is_valid: Optional[bool] index: int result: CheckSatResult +@dataclass(frozen=True) +class TestResult: + name: str # test function name + exitcode: int + num_models: int = None + models: List[ModelWithContext] = None + num_paths: Tuple[int, int, int] = None # number of paths: [total, success, blocked] + time: Tuple[int, int, int] = None # time: [total, paths, models] + num_bounded_loops: int = None # number of incomplete loops + + +class Exitcode(Enum): + PASS = 0 + COUNTEREXAMPLE = 1 + TIMEOUT = 2 + STUCK = 3 + REVERT_ALL = 4 + EXCEPTION = 5 + + +def is_global_fail_set(context: CallContext) -> bool: + hevm_fail = isinstance(context.output.error, FailCheatcode) + return hevm_fail or any(is_global_fail_set(x) for x in context.subcalls()) + + def run( setup_ex: Exec, abi: List, fun_info: FunctionInfo, - args: argparse.Namespace, -) -> int: + args: Namespace, +) -> TestResult: funname, funsig, funselector = fun_info.name, fun_info.sig, fun_info.selector if args.verbose >= 1: print(f"Executing {funname}") + dump_dirname = f"/tmp/{funname}-{uuid.uuid4().hex}" + # # calldata # cd = [] - wstore(cd, 0, 4, BitVecVal(int(funselector, 16), 32)) dyn_param_size = [] mk_calldata(abi, fun_info, cd, dyn_param_size, args) - # - # callvalue - # - - callvalue = mk_callvalue() + message = Message( + target=setup_ex.this, + caller=setup_ex.caller(), + value=con(0), + data=cd, + ) # # run # - start = timer() + timer = NamedTimer("time") + timer.create_subtimer("paths") options = mk_options(args) sevm = SEVM(options) - solver = SolverFor("QF_AUFBV") - solver.set(timeout=args.solver_timeout_branching) - solver.add(setup_ex.solver.assertions()) + solver = mk_solver(args) + path = Path(solver) + path.extend_path(setup_ex.path) - (exs, steps, bounded_loops) = sevm.run( + exs = sevm.run( Exec( code=setup_ex.code.copy(), # shallow copy storage=deepcopy(setup_ex.storage), @@ -580,33 +621,29 @@ def run( # block=deepcopy(setup_ex.block), # - calldata=cd, - callvalue=callvalue, - caller=setup_ex.caller, + context=CallContext(message=message), + callback=None, this=setup_ex.this, # + pgm=setup_ex.code[setup_ex.this], pc=0, st=State(), jumpis={}, - output=None, symbolic=args.symbolic_storage, prank=Prank(), # prank is reset after setUp() # - solver=solver, - path=deepcopy(setup_ex.path), + path=path, + alias=setup_ex.alias.copy(), # - log=deepcopy(setup_ex.log), cnts=deepcopy(setup_ex.cnts), - sha3s=deepcopy(setup_ex.sha3s), - storages=deepcopy(setup_ex.storages), - balances=deepcopy(setup_ex.balances), - calls=deepcopy(setup_ex.calls), - failed=setup_ex.failed, - error=setup_ex.error, + sha3s=setup_ex.sha3s.copy(), + storages=setup_ex.storages.copy(), + balances=setup_ex.balances.copy(), + calls=setup_ex.calls.copy(), ) ) - mid = timer() + (logs, steps) = (sevm.logs, sevm.steps) # check assertion violations normal = 0 @@ -614,98 +651,160 @@ def run( models: List[ModelWithContext] = [] stuck = [] + thread_pool = ThreadPoolExecutor(max_workers=args.solver_threads) + result_exs = [] + future_models = [] + counterexamples = [] + traces = {} + + def future_callback(future_model): + m = future_model.result() + models.append(m) + + model, is_valid, index, result = m.model, m.is_valid, m.index, m.result + if result == unsat: + return + + # model could be an empty dict here + if model is not None: + if is_valid: + print(red(f"Counterexample: {render_model(model)}")) + counterexamples.append(model) + else: + warn( + COUNTEREXAMPLE_INVALID, + f"Counterexample (potentially invalid): {render_model(model)}", + ) + counterexamples.append(model) + else: + warn(COUNTEREXAMPLE_UNKNOWN, f"Counterexample: {result}") + + if args.print_failed_states: + print(f"# {idx+1}") + print(result_exs[index]) + + if args.verbose >= VERBOSITY_TRACE_COUNTEREXAMPLE: + print( + f"Trace #{idx+1}:" + if args.verbose == VERBOSITY_TRACE_PATHS + else "Trace:" + ) + print(traces[index], end="") + for idx, ex in enumerate(exs): - opcode = ex.current_opcode() - if opcode in [EVM.STOP, EVM.RETURN]: + result_exs.append(ex) + + if args.verbose >= VERBOSITY_TRACE_PATHS: + print(f"Path #{idx+1}:") + print(indent_text(str(ex.path))) + + print("\nTrace:") + render_trace(ex.context) + + error = ex.context.output.error + + if ( + isinstance(error, Revert) + and unbox_int(ex.context.output.data) == ASSERT_FAIL + ) or is_global_fail_set(ex.context): + if args.verbose >= 1: + print(f"Found potential path (id: {idx+1})") + + if args.verbose >= VERBOSITY_TRACE_COUNTEREXAMPLE: + traces[idx] = rendered_trace(ex.context) + + query = ex.path.solver.to_smt2() + + future_model = thread_pool.submit( + gen_model_from_sexpr, GenModelArgs(args, idx, query, dump_dirname) + ) + future_model.add_done_callback(future_callback) + future_models.append(future_model) + + elif ex.context.is_stuck(): + stuck.append((idx, ex, ex.context.get_stuck_reason())) + if args.print_blocked_states: + traces[idx] = rendered_trace(ex.context) + + elif not error: normal += 1 - elif opcode in [EVM.REVERT, EVM.INVALID]: - # Panic(1) - # bytes4(keccak256("Panic(uint256)")) + bytes32(1) - if ( - unbox_int(ex.output) - == 0x4E487B710000000000000000000000000000000000000000000000000000000000000001 - ): - execs_to_model.append((idx, ex)) - elif ex.failed: - execs_to_model.append((idx, ex)) - else: - stuck.append((opcode, idx, ex)) - if len(execs_to_model) > 0 and args.verbose >= 1: + if len(result_exs) >= args.width: + break + + timer.create_subtimer("models") + + if len(future_models) > 0 and args.verbose >= 1: print( - f"# of potential paths involving assertion violations: {len(execs_to_model)} / {len(exs)}" + f"# of potential paths involving assertion violations: {len(future_models)} / {len(result_exs)} (--solver-threads {args.solver_threads})" ) - if len(execs_to_model) > 1 and args.solver_parallel: - if args.verbose >= 1: - print(f"Spawning {len(execs_to_model)} parallel assertion solvers") + if args.early_exit: + while not ( + len(counterexamples) > 0 or all([fm.done() for fm in future_models]) + ): + time.sleep(1) - fn_args = [ - GenModelArgs(args, idx, ex.solver.to_smt2()) for idx, ex in execs_to_model - ] - models = [m for m in thread_pool.map(gen_model_from_sexpr, fn_args)] + thread_pool.shutdown(wait=False, cancel_futures=True) else: - models = [gen_model(args, idx, ex) for idx, ex in execs_to_model] - - end = timer() - - no_counterexample = all(m.model is None for m in models) - passed = no_counterexample and normal > 0 and len(stuck) == 0 - passfail = color_good("[PASS]") if passed else color_warn("[FAIL]") + thread_pool.shutdown(wait=True) + + counter = Counter(str(m.result) for m in models) + if counter["sat"] > 0: + passfail = color_warn("[FAIL]") + exitcode = Exitcode.COUNTEREXAMPLE.value + elif counter["unknown"] > 0: + passfail = color_warn("[TIMEOUT]") + exitcode = Exitcode.TIMEOUT.value + elif len(stuck) > 0: + passfail = color_warn("[ERROR]") + exitcode = Exitcode.STUCK.value + elif normal == 0: + passfail = color_warn("[ERROR]") + exitcode = Exitcode.REVERT_ALL.value + warn( + REVERT_ALL, + f"{funsig}: all paths have been reverted; the setup state or inputs may have been too restrictive.", + ) + else: + passfail = color_good("[PASS]") + exitcode = Exitcode.PASS.value - time_info = f"{end - start:0.2f}s" - if args.statistics: - time_info += f" (paths: {mid - start:0.2f}s, models: {end - mid:0.2f}s)" + timer.stop() + time_info = timer.report(include_subtimers=args.statistics) # print result print( - f"{passfail} {funsig} (paths: {normal}/{len(exs)}, time: {time_info}, bounds: [{', '.join(dyn_param_size)}])" + f"{passfail} {funsig} (paths: {len(result_exs)}, {time_info}, bounds: [{', '.join(dyn_param_size)}])" ) - for m in models: - model, validity, idx, result = m.model, m.validity, m.index, m.result - if result == unsat: - continue - ex = exs[idx] - if model: - if validity: - print(color_warn(f"Counterexample: {model}")) - elif args.print_potential_counterexample: - warn( - COUNTEREXAMPLE_INVALID, - f"Counterexample (potentially invalid): {model}", - ) - else: - warn( - COUNTEREXAMPLE_INVALID, - f"Counterexample (potentially invalid): (not displayed, use --print-potential-counterexample)", - ) - else: - warn(COUNTEREXAMPLE_UNKNOWN, f"Counterexample: {result}") - - if args.print_failed_states: - print(f"# {idx+1} / {len(exs)}") - print(ex) - - for opcode, idx, ex in stuck: - print(color_warn(f"Not supported: {mnemonic(opcode)}: {ex.error}")) + for idx, ex, err in stuck: + warn(INTERNAL_ERROR, f"Encountered {err}") if args.print_blocked_states: - print(f"# {idx+1} / {len(exs)}") - print(ex) + print(f"\nPath #{idx+1}") + print(traces[idx], end="") - if bounded_loops: + if logs.bounded_loops: warn( LOOP_BOUND, f"{funsig}: paths have not been fully explored due to the loop unrolling bound: {args.loop}", ) if args.debug: - print("\n".join(bounded_loops)) + print("\n".join(logs.bounded_loops)) + + if logs.unknown_calls: + warn( + UNINTERPRETED_UNKNOWN_CALLS, + f"{funsig}: unknown calls have been assumed to be static: {', '.join(logs.unknown_calls)}", + ) + if args.debug: + logs.print_unknown_calls() # print post-states if args.print_states: - for idx, ex in enumerate(exs): - print(f"# {idx+1} / {len(exs)}") + for idx, ex in enumerate(result_exs): + print(f"# {idx+1} / {len(result_exs)}") print(ex) # log steps @@ -713,52 +812,66 @@ def run( with open(args.log, "w") as json_file: json.dump(steps, json_file) - # exitcode - return 0 if passed else 1 + # return test result + if args.minimal_json_output: + return TestResult(funsig, exitcode, len(counterexamples)) + else: + return TestResult( + funsig, + exitcode, + len(counterexamples), + counterexamples, + (len(result_exs), normal, len(stuck)), + (timer.elapsed(), timer["paths"].elapsed(), timer["models"].elapsed()), + len(logs.bounded_loops), + ) @dataclass(frozen=True) class SetupAndRunSingleArgs: - hexcode: str + creation_hexcode: str + deployed_hexcode: str abi: List setup_info: FunctionInfo fun_info: FunctionInfo - args: argparse.Namespace + setup_args: Namespace + args: Namespace + libs: Dict -def setup_and_run_single(fn_args: SetupAndRunSingleArgs) -> int: +def setup_and_run_single(fn_args: SetupAndRunSingleArgs) -> List[TestResult]: + args = fn_args.args try: setup_ex = setup( - fn_args.hexcode, + fn_args.creation_hexcode, + fn_args.deployed_hexcode, fn_args.abi, fn_args.setup_info, - fn_args.args, + fn_args.setup_args, + fn_args.libs, ) except Exception as err: - print( - color_warn( - f"Error: {fn_args.setup_info.sig} failed: {type(err).__name__}: {err}" - ) - ) + error(f"Error: {fn_args.setup_info.sig} failed: {type(err).__name__}: {err}") + if args.debug: traceback.print_exc() - return SETUP_FAILED + return [] try: - exitcode = run( + test_result = run( setup_ex, fn_args.abi, fn_args.fun_info, fn_args.args, ) except Exception as err: - print(f"{color_warn('[SKIP]')} {fn_args.fun_info.sig}") - print(color_warn(f"{type(err).__name__}: {err}")) + print(f"{color_warn('[ERROR]')} {fn_args.fun_info.sig}") + error(f"{type(err).__name__}: {err}") if args.debug: traceback.print_exc() - return TEST_FAILED + return [TestResult(fn_args.fun_info.sig, Exitcode.EXCEPTION.value)] - return exitcode + return [test_result] def extract_setup(methodIdentifiers: Dict[str, str]) -> FunctionInfo: @@ -784,145 +897,186 @@ class RunArgs: funsigs: List[str] # code of the current contract - hexcode: str + creation_hexcode: str + deployed_hexcode: str abi: List methodIdentifiers: Dict[str, str] + args: Namespace + contract_json: Dict + libs: Dict -def run_parallel(run_args: RunArgs) -> Tuple[int, int]: - hexcode, abi, methodIdentifiers = ( - run_args.hexcode, + +def run_parallel(run_args: RunArgs) -> List[TestResult]: + args = run_args.args + creation_hexcode, deployed_hexcode, abi, methodIdentifiers, libs = ( + run_args.creation_hexcode, + run_args.deployed_hexcode, run_args.abi, run_args.methodIdentifiers, + run_args.libs, ) setup_info = extract_setup(methodIdentifiers) - if setup_info.sig and args.verbose >= 1: - print(f"Running {setup_info.sig}") fun_infos = [ FunctionInfo(funsig.split("(")[0], funsig, methodIdentifiers[funsig]) for funsig in run_args.funsigs ] single_run_args = [ - SetupAndRunSingleArgs(hexcode, abi, setup_info, fun_info, args) + SetupAndRunSingleArgs( + creation_hexcode, + deployed_hexcode, + abi, + setup_info, + fun_info, + extend_args(args, parse_devdoc(setup_info.sig, run_args.contract_json)), + extend_args(args, parse_devdoc(fun_info.sig, run_args.contract_json)), + libs, + ) for fun_info in fun_infos ] # dispatch to the shared process pool - exitcodes = list(process_pool.map(setup_and_run_single, single_run_args)) - - num_passed = exitcodes.count(0) - num_failed = len(exitcodes) - num_passed + with ProcessPoolExecutor() as process_pool: + test_results = list(process_pool.map(setup_and_run_single, single_run_args)) + test_results = sum(test_results, []) # flatten lists - return num_passed, num_failed + return test_results -def run_sequential(run_args: RunArgs) -> Tuple[int, int]: +def run_sequential(run_args: RunArgs) -> List[TestResult]: + args = run_args.args setup_info = extract_setup(run_args.methodIdentifiers) - if setup_info.sig and args.verbose >= 1: - print(f"Running {setup_info.sig}") try: - setup_ex = setup(run_args.hexcode, run_args.abi, setup_info, args) + setup_args = extend_args( + args, parse_devdoc(setup_info.sig, run_args.contract_json) + ) + setup_ex = setup( + run_args.creation_hexcode, + run_args.deployed_hexcode, + run_args.abi, + setup_info, + setup_args, + run_args.libs, + ) except Exception as err: print( color_warn(f"Error: {setup_info.sig} failed: {type(err).__name__}: {err}") ) if args.debug: traceback.print_exc() - return (0, 0) + return [] - num_passed, num_failed = 0, 0 + test_results = [] for funsig in run_args.funsigs: fun_info = FunctionInfo( funsig.split("(")[0], funsig, run_args.methodIdentifiers[funsig] ) try: - exitcode = run(setup_ex, run_args.abi, fun_info, args) + extended_args = extend_args( + args, parse_devdoc(funsig, run_args.contract_json) + ) + test_result = run(setup_ex, run_args.abi, fun_info, extended_args) except Exception as err: - print(f"{color_warn('[SKIP]')} {funsig}") + print(f"{color_warn('[ERROR]')} {funsig}") print(color_warn(f"{type(err).__name__}: {err}")) if args.debug: traceback.print_exc() - num_failed += 1 + test_results.append(TestResult(funsig, Exitcode.EXCEPTION.value)) continue - if exitcode == 0: - num_passed += 1 - else: - num_failed += 1 - return (num_passed, num_failed) + test_results.append(test_result) + + return test_results + + +def extend_args(args: Namespace, more_opts: str) -> Namespace: + if more_opts: + new_args = deepcopy(args) + arg_parser.parse_args(more_opts.split(), new_args) + return new_args + else: + return args @dataclass(frozen=True) class GenModelArgs: - args: argparse.Namespace + args: Namespace idx: int sexpr: str + dump_dirname: Optional[str] = None -def gen_model_from_sexpr(fn_args: GenModelArgs) -> ModelWithContext: - args, idx, sexpr = fn_args.args, fn_args.idx, fn_args.sexpr - solver = SolverFor("QF_AUFBV", ctx=Context()) - solver.set(timeout=args.solver_timeout_assertion) - solver.from_string(sexpr) - res = solver.check() - model = solver.model() if res == sat else None +def copy_model(model: Model) -> Dict: + return {decl: model[decl] for decl in model} - # TODO: handle args.solver_subprocess - return package_result(model, idx, res, args) +def solve( + query: str, args: Namespace, dump_filename: Optional[str] = None +) -> Tuple[CheckSatResult, Model]: + if dump_filename: + with open(dump_filename, "w") as f: + f.write("(set-logic QF_AUFBV)\n") + f.write(query) + f.write("(get-model)\n") + solver = mk_solver(args, ctx=Context(), assertion=True) + solver.from_string(query) + result = solver.check() + model = copy_model(solver.model()) if result == sat else None + return result, model -def is_unknown(result: CheckSatResult, model: Model) -> bool: - return result == unknown or (result == sat and not is_valid_model(model)) +def gen_model_from_sexpr(fn_args: GenModelArgs) -> ModelWithContext: + args, idx, sexpr = fn_args.args, fn_args.idx, fn_args.sexpr + + dump_dirname = fn_args.dump_dirname if args.dump_smt_queries else None + dump_filename = f"{dump_dirname}/{idx+1}.smt2" if dump_dirname else None + if dump_dirname: + if not os.path.isdir(dump_dirname): + os.makedirs(dump_dirname) + print(f"Generating SMT queries in {dump_dirname}") -def gen_model(args: argparse.Namespace, idx: int, ex: Exec) -> ModelWithContext: if args.verbose >= 1: print(f"Checking path condition (path id: {idx+1})") - model = None - - ex.solver.set(timeout=args.solver_timeout_assertion) - res = ex.solver.check() - if res == sat: - model = ex.solver.model() + res, model = solve(sexpr, args, dump_filename) - if is_unknown(res, model) and args.solver_fresh: + if res == sat and not is_model_valid(model): if args.verbose >= 1: - print(f" Checking again with a fresh solver") - sol2 = SolverFor("QF_AUFBV", ctx=Context()) - # sol2.set(timeout=args.solver_timeout_assertion) - sol2.from_string(ex.solver.to_smt2()) - res = sol2.check() - if res == sat: - model = sol2.model() - - if is_unknown(res, model) and args.solver_subprocess: - if args.verbose >= 1: - print(f" Checking again in an external process") + print(f" Checking again with refinement") + + res, model = solve( + refine(sexpr), + args, + dump_filename.replace(".smt2", ".refined.smt2") if dump_filename else None, + ) + + if args.solver_subprocess and is_unknown(res, model): fname = f"/tmp/{uuid.uuid4().hex}.smt2" + if args.verbose >= 1: - print(f" z3 -model {fname} >{fname}.out") - query = ex.solver.to_smt2() - # replace uninterpreted abstraction with actual symbols for assertion solving - # TODO: replace `(evm_bvudiv x y)` with `(ite (= y (_ bv0 256)) (_ bv0 256) (bvudiv x y))` - # as bvudiv is undefined when y = 0; also similarly for evm_bvurem - query = re.sub(r"(\(\s*)evm_(bv[a-z]+)(_[0-9]+)?\b", r"\1\2", query) + print(f" Checking again in an external process") + print(f" {args.solver_subprocess_command} {fname} >{fname}.out") + + query = refine(sexpr) with open(fname, "w") as f: f.write("(set-logic QF_AUFBV)\n") f.write(query) - res_str = subprocess.run( - ["z3", "-model", fname], capture_output=True, text=True - ).stdout.strip() + + cmd = args.solver_subprocess_command.split() + [fname] + res_str = subprocess.run(cmd, capture_output=True, text=True).stdout.strip() res_str_head = res_str.split("\n", 1)[0] + with open(f"{fname}.out", "w") as f: f.write(res_str) + if args.verbose >= 1: print(f" {res_str_head}") + if res_str_head == "unsat": res = unsat elif res_str_head == "sat": @@ -932,11 +1086,29 @@ def gen_model(args: argparse.Namespace, idx: int, ex: Exec) -> ModelWithContext: return package_result(model, idx, res, args) +def is_unknown(result: CheckSatResult, model: Model) -> bool: + return result == unknown or (result == sat and not is_model_valid(model)) + + +def refine(query: str) -> str: + # replace uninterpreted abstraction with actual symbols for assertion solving + # TODO: replace `(evm_bvudiv x y)` with `(ite (= y (_ bv0 256)) (_ bv0 256) (bvudiv x y))` + # as bvudiv is undefined when y = 0; also similarly for evm_bvurem + query = re.sub(r"(\(\s*)evm_(bv[a-z]+)(_[0-9]+)?\b", r"\1\2", query) + # remove the uninterpreted function symbols + # TODO: this will be no longer needed once is_model_valid is properly implemented + return re.sub( + r"\(\s*declare-fun\s+evm_(bv[a-z]+)(_[0-9]+)?\b", + r"(declare-fun dummy_\1\2", + query, + ) + + def package_result( - model: UnionType[Model, str], + model: Optional[UnionType[Model, str]], idx: int, result: CheckSatResult, - args: argparse.Namespace, + args: Namespace, ) -> ModelWithContext: if result == unsat: if args.verbose >= 1: @@ -948,16 +1120,16 @@ def package_result( print(f" Valid path; counterexample generated (path id: {idx+1})") # convert model into string to avoid pickling errors for z3 (ctypes) objects containing pointers - validity = None + is_valid = None if model: if isinstance(model, str): - validity = True + is_valid = True model = f"see {model}" else: - validity = is_valid_model(model) - model = f"{str_model(model, args)}" + is_valid = is_model_valid(model) + model = to_str_model(model, args.print_full_model) - return ModelWithContext(model, validity, idx, result) + return ModelWithContext(model, is_valid, idx, result) else: if args.verbose >= 1: @@ -965,46 +1137,46 @@ def package_result( return ModelWithContext(None, None, idx, result) -def is_valid_model(model) -> bool: +def is_model_valid(model: AnyModel) -> bool: + # TODO: evaluate the path condition against the given model after excluding evm_* symbols, + # since the evm_* symbols may still appear in valid models. for decl in model: if str(decl).startswith("evm_"): return False return True -def str_model(model, args: argparse.Namespace) -> str: +def to_str_model(model: Model, print_full_model: bool) -> StrModel: def select(var): name = str(var) - if name.startswith("p_"): - return True - if name.startswith("halmos_"): - return True - return False - - select_model = filter(select, model) if not args.print_full_model else model - result = ",".join( - sorted(map(lambda decl: f"\n {decl} = {hexify(model[decl])}", select_model)) - ) - return f"[{result}]" + return name.startswith("p_") or name.startswith("halmos_") + + select_model = filter(select, model) if not print_full_model else model + return {str(decl): stringify(str(decl), model[decl]) for decl in select_model} + +def render_model(model: UnionType[str, StrModel]) -> str: + if isinstance(model, str): + return model -def mk_options(args: argparse.Namespace) -> Dict: + formatted = [f"\n {decl} = {val}" for decl, val in model.items()] + return "".join(sorted(formatted)) if formatted else "∅" + + +def mk_options(args: Namespace) -> Dict: options = { "target": args.root, "verbose": args.verbose, "debug": args.debug, "log": args.log, - "add": not args.no_smt_add, - "sub": not args.no_smt_sub, - "mul": not args.no_smt_mul, - "div": args.smt_div, - "mod": args.smt_mod, - "divByConst": args.smt_div_by_const, - "modByConst": args.smt_mod_by_const, "expByConst": args.smt_exp_by_const, "timeout": args.solver_timeout_branching, + "max_memory": args.solver_max_memory, "sym_jump": args.symbolic_jump, "print_steps": args.print_steps, + "unknown_calls_return_size": args.return_size_of_unknown_calls, + "ffi": args.ffi, + "storage_layout": args.storage_layout, } if args.width is not None: @@ -1016,10 +1188,15 @@ def mk_options(args: argparse.Namespace) -> Dict: if args.loop is not None: options["max_loop"] = args.loop + options["unknown_calls"] = [] + if args.uninterpreted_unknown_calls.strip(): + for x in args.uninterpreted_unknown_calls.split(","): + options["unknown_calls"].append(int(x, 0)) + return options -def mk_arrlen(args: argparse.Namespace) -> Dict[str, int]: +def mk_arrlen(args: Namespace) -> Dict[str, int]: arrlen = {} if args.array_lengths: for assign in [x.split("=") for x in args.array_lengths.split(",")]: @@ -1029,13 +1206,13 @@ def mk_arrlen(args: argparse.Namespace) -> Dict[str, int]: return arrlen -def parse_build_out(args: argparse.Namespace) -> Dict: +def parse_build_out(args: Namespace) -> Dict: result = {} # compiler version -> source filename -> contract name -> (json, type) out_path = os.path.join(args.root, args.forge_build_out) if not os.path.exists(out_path): raise FileNotFoundError( - f"the build output directory `{out_path}` does not exist" + f"The build output directory `{out_path}` does not exist" ) for sol_dirname in os.listdir(out_path): # for each source filename @@ -1047,50 +1224,150 @@ def parse_build_out(args: argparse.Namespace) -> Dict: continue for json_filename in os.listdir(sol_path): # for each contract name - if not json_filename.endswith(".json"): - continue - if json_filename.startswith("."): + try: + if not json_filename.endswith(".json"): + continue + if json_filename.startswith("."): + continue + + json_path = os.path.join(sol_path, json_filename) + with open(json_path, encoding="utf8") as f: + json_out = json.load(f) + + compiler_version = json_out["metadata"]["compiler"]["version"] + if compiler_version not in result: + result[compiler_version] = {} + if sol_dirname not in result[compiler_version]: + result[compiler_version][sol_dirname] = {} + contract_map = result[compiler_version][sol_dirname] + + # cut off compiler version number as well + contract_name = json_filename.split(".")[0] + + contract_type = None + for node in json_out["ast"]["nodes"]: + if ( + node["nodeType"] == "ContractDefinition" + and node["name"] == contract_name + ): + abstract = "abstract " if node.get("abstract") else "" + contract_type = abstract + node["contractKind"] + natspec = node.get("documentation") + break + if contract_type is None: + raise ValueError("no contract type", contract_name) + + if contract_name in contract_map: + raise ValueError( + "duplicate contract names in the same file", + contract_name, + sol_dirname, + ) + contract_map[contract_name] = (json_out, contract_type, natspec) + except Exception as err: + print( + color_warn( + f"Skipped {json_filename} due to parsing failure: {type(err).__name__}: {err}" + ) + ) + if args.debug: + traceback.print_exc() continue - json_path = os.path.join(sol_path, json_filename) - with open(json_path, encoding="utf8") as f: - json_out = json.load(f) + return result + + +def parse_devdoc(funsig: str, contract_json: Dict) -> str: + try: + return contract_json["metadata"]["output"]["devdoc"]["methods"][funsig][ + "custom:halmos" + ] + except KeyError as err: + return None - compiler_version = json_out["metadata"]["compiler"]["version"] - if compiler_version not in result: - result[compiler_version] = {} - if sol_dirname not in result[compiler_version]: - result[compiler_version][sol_dirname] = {} - contract_map = result[compiler_version][sol_dirname] - # cut off compiler version number as well - contract_name = json_filename.split(".")[0] +def parse_natspec(natspec: Dict) -> str: + # This parsing scheme is designed to handle: + # + # - multiline tags: + # /// @custom:halmos --x + # /// --y + # + # - multiple tags: + # /// @custom:halmos --x + # /// @custom:halmos --y + # + # - tags that start in the middle of line: + # /// blah blah @custom:halmos --x + # /// --y + # + # In all the above examples, this scheme returns "--x (whitespaces) --y" + isHalmosTag = False + result = "" + for item in re.split(r"(@\S+)", natspec.get("text", "")): + if item == "@custom:halmos": + isHalmosTag = True + elif re.match(r"^@\S", item): + isHalmosTag = False + elif isHalmosTag: + result += item + return result.strip() - contract_type = None - for node in json_out["ast"]["nodes"]: - if ( - node["nodeType"] == "ContractDefinition" - and node["name"] == contract_name - ): - abstract = "abstract " if node.get("abstract") else "" - contract_type = abstract + node["contractKind"] - break - if contract_type is None: - raise ValueError("no contract type", contract_name) - - if contract_name in contract_map: - raise ValueError( - "duplicate contract names in the same file", - contract_name, - sol_dirname, - ) - contract_map[contract_name] = (json_out, contract_type) - return result +def import_libs(build_out_map: Dict, hexcode: str, linkReferences: Dict) -> Dict: + libs = {} + for filepath in linkReferences: + file_name = filepath.split("/")[-1] -def main() -> int: - main_start = timer() + for lib_name in linkReferences[filepath]: + (lib_json, _, _) = build_out_map[file_name][lib_name] + lib_hexcode = lib_json["deployedBytecode"]["object"] + + # in bytes, multiply indices by 2 and offset 0x + placeholder_index = linkReferences[filepath][lib_name][0]["start"] * 2 + 2 + placeholder = hexcode[placeholder_index : placeholder_index + 40] + + libs[f"{filepath}:{lib_name}"] = { + "placeholder": placeholder, + "hexcode": lib_hexcode, + } + + return libs + + +def build_output_iterator(build_out: Dict): + for compiler_version in sorted(build_out): + build_out_map = build_out[compiler_version] + for filename in sorted(build_out_map): + for contract_name in sorted(build_out_map[filename]): + yield (build_out_map, filename, contract_name) + + +def contract_regex(args): + if args.contract: + return f"^{args.contract}$" + else: + return args.match_contract + + +def test_regex(args): + if args.match_test.startswith("^"): + return args.match_test + else: + return f"^{args.function}.*{args.match_test}" + + +@dataclass(frozen=True) +class MainResult: + exitcode: int + # contract path -> list of test results + test_results: Dict[str, List[TestResult]] = None + + +def _main(_args=None) -> MainResult: + timer = NamedTimer("total") + timer.create_subtimer("build") # # z3 global options @@ -1104,17 +1381,16 @@ def main() -> int: # command line arguments # - global args - args = parse_args() + args = arg_parser.parse_args(_args) if args.version: print(f"Halmos {metadata.version('halmos')}") - return 0 + return MainResult(0) # quick bytecode execution mode if args.bytecode is not None: - run_bytecode(args.bytecode) - return 0 + run_bytecode(args.bytecode, args) + return MainResult(0) # # compile @@ -1123,6 +1399,7 @@ def main() -> int: build_cmd = [ "forge", # shutil.which('forge') "build", + "--build-info", "--root", args.root, "--extra-output", @@ -1134,87 +1411,134 @@ def main() -> int: build_exitcode = subprocess.run(build_cmd).returncode if build_exitcode: - print(color_warn(f"build failed: {build_cmd}")) - return 1 + print(color_warn(f"Build failed: {build_cmd}")) + return MainResult(1) + timer.create_subtimer("load") try: build_out = parse_build_out(args) except Exception as err: - print(color_warn(f"build output parsing failed: {err}")) - return 1 - - main_mid = timer() + print(color_warn(f"Build output parsing failed: {type(err).__name__}: {err}")) + if args.debug: + traceback.print_exc() + return MainResult(1) - # - # run - # + timer.create_subtimer("tests") total_passed = 0 total_failed = 0 total_found = 0 + test_results_map = {} - for compiler_version in sorted(build_out): - build_out_map = build_out[compiler_version] - for filename in sorted(build_out_map): - for contract_name in sorted(build_out_map[filename]): - if args.contract and args.contract != contract_name: - continue + # + # exit and signal handlers to avoid dropping json output + # - (contract_json, contract_type) = build_out_map[filename][contract_name] - if contract_type != "contract": - continue + def on_exit(exitcode: int) -> MainResult: + result = MainResult(exitcode, test_results_map) - hexcode = contract_json["deployedBytecode"]["object"] - abi = contract_json["abi"] - methodIdentifiers = contract_json["methodIdentifiers"] + if args.json_output: + if args.debug: + debug(f"Writing output to {args.json_output}") + with open(args.json_output, "w") as json_file: + json.dump(asdict(result), json_file, indent=4) - funsigs = [ - funsig - for funsig in methodIdentifiers - if funsig.startswith(args.function) - ] + return result - if funsigs: - total_found += len(funsigs) - print( - f"\nRunning {len(funsigs)} tests for {contract_json['ast']['absolutePath']}:{contract_name}" - ) - contract_start = timer() - - run_args = RunArgs(funsigs, hexcode, abi, methodIdentifiers) - enable_parallel = args.test_parallel and len(funsigs) > 1 - num_passed, num_failed = ( - run_parallel(run_args) - if enable_parallel - else run_sequential(run_args) - ) + def on_signal(signum, frame): + if args.debug: + debug(f"Signal {signum} received") + exitcode = 128 + signum + on_exit(exitcode) + sys.exit(exitcode) - print( - f"Symbolic test result: {num_passed} passed; {num_failed} failed; time: {timer() - contract_start:0.2f}s" - ) - total_passed += num_passed - total_failed += num_failed + for signum in [signal.SIGINT, signal.SIGTERM]: + signal.signal(signum, on_signal) - main_end = timer() + # + # run + # + + for build_out_map, filename, contract_name in build_output_iterator(build_out): + if not re.search(contract_regex(args), contract_name): + continue + + (contract_json, contract_type, natspec) = build_out_map[filename][contract_name] + if contract_type != "contract": + continue + + methodIdentifiers = contract_json["methodIdentifiers"] + funsigs = [f for f in methodIdentifiers if re.search(test_regex(args), f)] + num_found = len(funsigs) + + if num_found == 0: + continue + + contract_timer = NamedTimer("time") + + abi = contract_json["abi"] + creation_hexcode = contract_json["bytecode"]["object"] + deployed_hexcode = contract_json["deployedBytecode"]["object"] + linkReferences = contract_json["bytecode"]["linkReferences"] + libs = import_libs(build_out_map, creation_hexcode, linkReferences) + + contract_path = f"{contract_json['ast']['absolutePath']}:{contract_name}" + print(f"\nRunning {num_found} tests for {contract_path}") + contract_args = extend_args(args, parse_natspec(natspec)) if natspec else args + + run_args = RunArgs( + funsigs, + creation_hexcode, + deployed_hexcode, + abi, + methodIdentifiers, + contract_args, + contract_json, + libs, + ) + + enable_parallel = args.test_parallel and num_found > 1 + run_method = run_parallel if enable_parallel else run_sequential + test_results = run_method(run_args) + + num_passed = sum(r.exitcode == 0 for r in test_results) + num_failed = num_found - num_passed - if args.statistics: print( - f"\n[time] total: {main_end - main_start:0.2f}s (build: {main_mid - main_start:0.2f}s, tests: {main_end - main_mid:0.2f}s)" + f"Symbolic test result: {num_passed} passed; " + f"{num_failed} failed; {contract_timer.report()}" ) + total_found += num_found + total_passed += num_passed + total_failed += num_failed + + if contract_path in test_results_map: + raise ValueError("already exists", contract_path) + + test_results_map[contract_path] = test_results + + if args.statistics: + print(f"\n[time] {timer.report()}") + if total_found == 0: - error_msg = f"Error: No tests with the prefix `{args.function}`" - if args.contract is not None: - error_msg += f" in {args.contract}" + error_msg = ( + f"Error: No tests with" + + f" --match-contract '{contract_regex(args)}'" + + f" --match-test '{test_regex(args)}'" + ) print(color_warn(error_msg)) - return 1 + return MainResult(1) - # exitcode - if total_failed == 0: - return 0 - else: - return 1 + exitcode = 0 if total_failed == 0 else 1 + return on_exit(exitcode) + + +# entrypoint for the `halmos` script +def main() -> int: + return _main().exitcode +# entrypoint for `python -m halmos` if __name__ == "__main__": sys.exit(main()) diff --git a/src/halmos/calldata.py b/src/halmos/calldata.py new file mode 100644 index 00000000..92aa66a6 --- /dev/null +++ b/src/halmos/calldata.py @@ -0,0 +1,206 @@ +# SPDX-License-Identifier: AGPL-3.0 + +import re + +from dataclasses import dataclass +from typing import List, Dict +from argparse import Namespace +from functools import reduce + +from z3 import * + +from .sevm import con, concat + + +@dataclass(frozen=True) +class Type: + var: str + + +@dataclass(frozen=True) +class BaseType(Type): + typ: str + + +@dataclass(frozen=True) +class FixedArrayType(Type): + base: Type + size: int + + +@dataclass(frozen=True) +class DynamicArrayType(Type): + base: Type + + +@dataclass(frozen=True) +class TupleType(Type): + items: List[Type] + + +def parse_type(var: str, typ: str, item: Dict) -> Type: + """Parse ABI type in JSON format""" + + # parse array type + match = re.search(r"^(.*)(\[([0-9]*)\])$", typ) + if match: + base_type = match.group(1) + array_len = match.group(3) + + # recursively parse base type + base = parse_type("", base_type, item) + + if array_len == "": # dynamic array + return DynamicArrayType(var, base) + else: + return FixedArrayType(var, base, int(array_len)) + + # check supported type + match = re.search(r"^(u?int[0-9]*|address|bool|bytes[0-9]*|string|tuple)$", typ) + if not match: + # TODO: support fixedMxN, ufixedMxN, function types + raise NotImplementedError(f"Not supported type: {typ}") + + # parse tuple type + if typ == "tuple": + return parse_tuple_type(var, item["components"]) + + # parse primitive types + return BaseType(var, typ) + + +def parse_tuple_type(var: str, items: List[Dict]) -> Type: + parsed_items = [parse_type(item["name"], item["type"], item) for item in items] + return TupleType(var, parsed_items) + + +@dataclass(frozen=True) +class EncodingResult: + data: List[BitVecRef] + size: int + static: bool + + +class Calldata: + args: Namespace + arrlen: Dict[str, int] + dyn_param_size: List[str] # to be updated + + def __init__( + self, args: Namespace, arrlen: Dict[str, int], dyn_param_size: List[str] + ) -> None: + self.args = args + self.arrlen = arrlen + self.dyn_param_size = dyn_param_size + + def choose_array_len(self, name: str) -> int: + if name in self.arrlen: + array_len = self.arrlen[name] + else: + array_len = self.args.loop + if self.args.debug: + print( + f"Warning: no size provided for {name}; default value {array_len} will be used." + ) + + self.dyn_param_size.append(f"|{name}|={array_len}") + + return array_len + + def create(self, abi: Dict) -> BitVecRef: + """Create calldata of ABI type""" + + # list of parameter types + tuple_type = parse_tuple_type("", abi["inputs"]) + + # no parameters + if len(tuple_type.items) == 0: + return None + + # ABI encoded symbolic calldata for parameters + encoded = self.encode("", tuple_type) + result = concat(encoded.data) + + # sanity check + if result.size() != 8 * encoded.size: + raise ValueError(encoded) + + return result + + def encode(self, name: str, typ: Type) -> EncodingResult: + """Create symbolic ABI encoded calldata + + See https://docs.soliditylang.org/en/latest/abi-spec.html + """ + + # (T1, T2, ..., Tn) + if isinstance(typ, TupleType): + prefix = f"{name}." if name else "" + items = [self.encode(f"{prefix}{item.var}", item) for item in typ.items] + return self.encode_tuple(items) + + # T[k] + if isinstance(typ, FixedArrayType): + items = [self.encode(f"{name}[{i}]", typ.base) for i in range(typ.size)] + return self.encode_tuple(items) + + # T[] + if isinstance(typ, DynamicArrayType): + array_len = self.choose_array_len(name) + items = [self.encode(f"{name}[{i}]", typ.base) for i in range(array_len)] + encoded = self.encode_tuple(items) + return EncodingResult( + [con(array_len)] + encoded.data, 32 + encoded.size, False + ) + + if isinstance(typ, BaseType): + # bytes, string + if typ.typ in ["bytes", "string"]: + size = 65 # ECDSA signature size # TODO: use args + size_pad_right = ((size + 31) // 32) * 32 + data = [ + con(size), + BitVec(f"p_{name}_{typ.typ}", 8 * size_pad_right), + ] + return EncodingResult(data, 32 + size_pad_right, False) + + # uintN, intN, address, bool, bytesN + else: + return EncodingResult([BitVec(f"p_{name}_{typ.typ}", 256)], 32, True) + + raise ValueError(typ) + + def encode_tuple(self, items: List[EncodingResult]) -> EncodingResult: + # For X = (X(1), ..., X(k)): + # + # enc(X) = head(X(1)) ... head(X(k)) tail(X(1)) ... tail(X(k)) + # + # if Ti is static: + # head(X(i)) = enc(X(i)) + # tail(X(i)) = "" (the empty string) + # + # if Ti is dynamic: + # head(X(i)) = enc(len( head(X(1)) ... head(X(k)) tail(X(1)) ... tail(X(i-1)) )) + # tail(X(i)) = enc(X(i)) + # + # See https://docs.soliditylang.org/en/latest/abi-spec.html#formal-specification-of-the-encoding + + # compute total head size + head_size = lambda x: x.size if x.static else 32 + total_head_size = reduce(lambda s, x: s + head_size(x), items, 0) + + # generate heads and tails + total_size = total_head_size + heads, tails = [], [] + for item in items: + if item.static: + heads.extend(item.data) + else: + heads.append(con(total_size)) + tails.extend(item.data) + total_size += item.size + + # tuple is static if all elements are static + static = len(tails) == 0 + + return EncodingResult(heads + tails, total_size, static) diff --git a/src/halmos/cheatcodes.py b/src/halmos/cheatcodes.py index c4ed16ee..9a49e0ea 100644 --- a/src/halmos/cheatcodes.py +++ b/src/halmos/cheatcodes.py @@ -1,10 +1,64 @@ # SPDX-License-Identifier: AGPL-3.0 +import json +import re + +from subprocess import Popen, PIPE from typing import List, Dict, Set, Tuple, Any from z3 import * -from .utils import assert_address, con_addr +from .exceptions import FailCheatcode, HalmosException +from .utils import * + + +def name_of(x: str) -> str: + return re.sub(r"\s+", "_", x) + + +def extract_string_array_argument(calldata: BitVecRef, arg_idx: int): + """Extracts idx-th argument of string array from calldata""" + + array_slot = int_of(extract_bytes(calldata, 4 + 32 * arg_idx, 32)) + num_strings = int_of(extract_bytes(calldata, 4 + array_slot, 32)) + + string_array = [] + + for i in range(num_strings): + string_offset = int_of( + extract_bytes(calldata, 4 + array_slot + 32 * (i + 1), 32) + ) + string_length = int_of( + extract_bytes(calldata, 4 + array_slot + 32 + string_offset, 32) + ) + string_value = int_of( + extract_bytes( + calldata, 4 + array_slot + 32 + string_offset + 32, string_length + ) + ) + string_bytes = string_value.to_bytes(string_length, "big") + string_array.append(string_bytes.decode("utf-8")) + + return string_array + + +def stringified_bytes_to_bytes(hexstring: str): + """Converts a string of bytes to a bytes memory type""" + + hexstring = stripped(hexstring) + hexstring_len = (len(hexstring) + 1) // 2 + hexstring_len_enc = stripped(hex(hexstring_len)).rjust(64, "0") + hexstring_len_ceil = (hexstring_len + 31) // 32 * 32 + + ret_bytes = bytes.fromhex( + "00" * 31 + + "20" + + hexstring_len_enc + + hexstring.ljust(hexstring_len_ceil * 2, "0") + ) + ret_len = len(ret_bytes) + + return BitVecVal(int.from_bytes(ret_bytes, "big"), ret_len * 8) class Prank: @@ -20,9 +74,9 @@ def __init__(self, addr: Any = None, keep: bool = False) -> None: def __str__(self) -> str: if self.addr: if self.keep: - return f"startPrank({str(addr)})" + return f"startPrank({str(self.addr)})" else: - return f"prank({str(addr)})" + return f"prank({str(self.addr)})" else: return "None" @@ -63,28 +117,107 @@ def stopPrank(self) -> bool: return True -class halmos_cheat_code: - # address constant SVM_ADDRESS = - # address(bytes20(uint160(uint256(keccak256('svm cheat code'))))); - address: BitVecRef = con_addr(0xF3993A62377BCD56AE39D773740A5390411E8BC9) +def create_generic(ex, bits: int, var_name: str, type_name: str) -> BitVecRef: + label = f"halmos_{var_name}_{type_name}_{ex.new_symbol_id():>02}" + return BitVec(label, BitVecSorts[bits]) - # bytes4(keccak256("createUint(uint256,string)")) - create_uint: int = 0x66830DFA - # bytes4(keccak256("createBytes(uint256,string)")) - create_bytes: int = 0xEEF5311D +def create_uint(ex, arg): + bits = int_of( + extract_bytes(arg, 4, 32), "symbolic bit size for halmos.createUint()" + ) + if bits > 256: + raise HalmosException(f"bitsize larger than 256: {bits}") + + name = name_of(extract_string_argument(arg, 1)) + return uint256(create_generic(ex, bits, name, f"uint{bits}")) - # bytes4(keccak256("createUint256(string)")) - create_uint256: int = 0xBC7BEEFC - # bytes4(keccak256("createBytes32(string)")) - create_bytes32: int = 0xBF72FA66 +def create_uint256(ex, arg): + name = name_of(extract_string_argument(arg, 0)) + return create_generic(ex, 256, name, "uint256") - # bytes4(keccak256("createAddress(string)")) - create_address: int = 0x3B0FA01B - # bytes4(keccak256("createBool(string)")) - create_bool: int = 0x6E0BB659 +def create_int(ex, arg): + bits = int_of( + extract_bytes(arg, 4, 32), "symbolic bit size for halmos.createUint()" + ) + if bits > 256: + raise HalmosException(f"bitsize larger than 256: {bits}") + + name = name_of(extract_string_argument(arg, 1)) + return int256(create_generic(ex, bits, name, f"int{bits}")) + + +def create_int256(ex, arg): + name = name_of(extract_string_argument(arg, 0)) + return create_generic(ex, 256, name, "int256") + + +def create_bytes(ex, arg): + byte_size = int_of( + extract_bytes(arg, 4, 32), "symbolic byte size for halmos.createBytes()" + ) + name = name_of(extract_string_argument(arg, 1)) + symbolic_bytes = create_generic(ex, byte_size * 8, name, "bytes") + return Concat(con(32), con(byte_size), symbolic_bytes) + + +def create_string(ex, arg): + byte_size = int_of( + extract_bytes(arg, 4, 32), "symbolic byte size for halmos.createString()" + ) + name = name_of(extract_string_argument(arg, 1)) + symbolic_string = create_generic(ex, byte_size * 8, name, "string") + return Concat(con(32), con(byte_size), symbolic_string) + + +def create_bytes4(ex, arg): + name = name_of(extract_string_argument(arg, 0)) + return uint256(create_generic(ex, 32, name, "bytes4")) + + +def create_bytes32(ex, arg): + name = name_of(extract_string_argument(arg, 0)) + return create_generic(ex, 256, name, "bytes32") + + +def create_address(ex, arg): + name = name_of(extract_string_argument(arg, 0)) + return uint256(create_generic(ex, 160, name, "address")) + + +def create_bool(ex, arg): + name = name_of(extract_string_argument(arg, 0)) + return uint256(create_generic(ex, 1, name, "bool")) + + +class halmos_cheat_code: + # address constant SVM_ADDRESS = + # address(bytes20(uint160(uint256(keccak256('svm cheat code'))))); + address: BitVecRef = con_addr(0xF3993A62377BCD56AE39D773740A5390411E8BC9) + + handlers = { + 0x66830DFA: create_uint, # createUint(uint256,string) + 0xBC7BEEFC: create_uint256, # createUint256(string) + 0x49B9C7D4: create_int, # createInt(uint256,string) + 0xC2CE6AED: create_int256, # createInt256(string) + 0xEEF5311D: create_bytes, # createBytes(uint256,string) + 0xCE68656C: create_string, # createString(uint256,string) + 0xDE143925: create_bytes4, # createBytes4(string) + 0xBF72FA66: create_bytes32, # createBytes32(string) + 0x3B0FA01B: create_address, # createAddress(string) + 0x6E0BB659: create_bool, # createBool(string) + } + + @staticmethod + def handle(ex, arg: BitVecRef) -> BitVecRef: + funsig = int_of(extract_funsig(arg), "symbolic halmos cheatcode") + if handler := halmos_cheat_code.handlers.get(funsig): + return handler(ex, arg) + + error_msg = f"Unknown halmos cheat code: function selector = 0x{funsig:0>8x}, calldata = {hexify(arg)}" + raise HalmosException(error_msg) class hevm_cheat_code: @@ -151,11 +284,171 @@ class hevm_cheat_code: # bytes4(keccak256("etch(address,bytes)")) etch_sig: int = 0xB4D6C782 + # bytes4(keccak256("ffi(string[])")) + ffi_sig: int = 0x89160467 + + @staticmethod + def handle(sevm, ex, arg: BitVec) -> BitVec: + funsig: int = int_of(extract_funsig(arg), "symbolic hevm cheatcode") + + # vm.fail() + # BitVecVal(hevm_cheat_code.fail_payload, 800) + if arg == hevm_cheat_code.fail_payload: + raise FailCheatcode() + + # vm.assume(bool) + elif ( + eq(arg.sort(), BitVecSorts[(4 + 32) * 8]) + and funsig == hevm_cheat_code.assume_sig + ): + assume_cond = simplify(is_non_zero(Extract(255, 0, arg))) + ex.path.append(assume_cond) + + # vm.getCode(string) + elif funsig == hevm_cheat_code.get_code_sig: + calldata = bv_value_to_bytes(arg) + path_len = int.from_bytes(calldata[36:68], "big") + path = calldata[68 : 68 + path_len].decode("utf-8") + + if ":" in path: + [filename, contract_name] = path.split(":") + path = "out/" + filename + "/" + contract_name + ".json" -class console: - # address constant CONSOLE_ADDRESS = address(0x000000000000000000636F6e736F6c652e6c6f67); - address: BitVecRef = con_addr(0x000000000000000000636F6E736F6C652E6C6F67) + target = sevm.options["target"].rstrip("/") + path = target + "/" + path - log_uint: int = 0xF5B1BBA9 # bytes4(keccak256("log(uint)")) + with open(path) as f: + artifact = json.loads(f.read()) - log_string: int = 0x41304FAC # bytes4(keccak256("log(string)")) + if artifact["bytecode"]["object"]: + bytecode = artifact["bytecode"]["object"].replace("0x", "") + else: + bytecode = artifact["bytecode"].replace("0x", "") + + return stringified_bytes_to_bytes(bytecode) + + # vm.prank(address) + elif funsig == hevm_cheat_code.prank_sig: + result = ex.prank.prank(uint160(Extract(255, 0, arg))) + if not result: + raise HalmosException("You have an active prank already.") + + # vm.startPrank(address) + elif funsig == hevm_cheat_code.start_prank_sig: + result = ex.prank.startPrank(uint160(Extract(255, 0, arg))) + if not result: + raise HalmosException("You have an active prank already.") + + # vm.stopPrank() + elif funsig == hevm_cheat_code.stop_prank_sig: + ex.prank.stopPrank() + + # vm.deal(address,uint256) + elif funsig == hevm_cheat_code.deal_sig: + who = uint160(Extract(511, 256, arg)) + amount = simplify(Extract(255, 0, arg)) + ex.balance_update(who, amount) + + # vm.store(address,bytes32,bytes32) + elif funsig == hevm_cheat_code.store_sig: + store_account = uint160(Extract(767, 512, arg)) + store_slot = simplify(Extract(511, 256, arg)) + store_value = simplify(Extract(255, 0, arg)) + store_account_addr = sevm.resolve_address_alias(ex, store_account) + if store_account_addr is not None: + sevm.sstore(ex, store_account_addr, store_slot, store_value) + else: + raise HalmosException(f"uninitialized account: {store_account}") + + # vm.load(address,bytes32) + elif funsig == hevm_cheat_code.load_sig: + load_account = uint160(Extract(511, 256, arg)) + load_slot = simplify(Extract(255, 0, arg)) + load_account_addr = sevm.resolve_address_alias(ex, load_account) + if load_account_addr is not None: + return sevm.sload(ex, load_account_addr, load_slot) + else: + raise HalmosException(f"uninitialized account: {store_account}") + + # vm.fee(uint256) + elif funsig == hevm_cheat_code.fee_sig: + ex.block.basefee = simplify(Extract(255, 0, arg)) + + # vm.chainId(uint256) + elif funsig == hevm_cheat_code.chainid_sig: + ex.block.chainid = simplify(Extract(255, 0, arg)) + + # vm.coinbase(address) + elif funsig == hevm_cheat_code.coinbase_sig: + ex.block.coinbase = uint160(Extract(255, 0, arg)) + + # vm.difficulty(uint256) + elif funsig == hevm_cheat_code.difficulty_sig: + ex.block.difficulty = simplify(Extract(255, 0, arg)) + + # vm.roll(uint256) + elif funsig == hevm_cheat_code.roll_sig: + ex.block.number = simplify(Extract(255, 0, arg)) + + # vm.warp(uint256) + elif funsig == hevm_cheat_code.warp_sig: + ex.block.timestamp = simplify(Extract(255, 0, arg)) + + # vm.etch(address,bytes) + elif funsig == hevm_cheat_code.etch_sig: + who = extract_bytes(arg, 4 + 12, 20) + + # who must be concrete + if not is_bv_value(who): + error_msg = f"vm.etch(address who, bytes code) must have concrete argument `who` but received {who}" + raise HalmosException(error_msg) + + # code must be concrete + try: + code_offset = int_of(extract_bytes(arg, 4 + 32, 32)) + code_length = int_of(extract_bytes(arg, 4 + code_offset, 32)) + code_int = int_of(extract_bytes(arg, 4 + code_offset + 32, code_length)) + code_bytes = code_int.to_bytes(code_length, "big") + ex.set_code(who, code_bytes) + except Exception as e: + error_msg = f"vm.etch(address who, bytes code) must have concrete argument `code` but received calldata {arg}" + raise HalmosException(error_msg) from e + + # ffi(string[]) returns (bytes) + elif funsig == hevm_cheat_code.ffi_sig: + if not sevm.options.get("ffi"): + error_msg = "ffi cheatcode is disabled. Run again with `--ffi` if you want to enable it" + raise HalmosException(error_msg) + + cmd = extract_string_array_argument(arg, 0) + + debug = sevm.options.get("debug", False) + verbose = sevm.options.get("verbose", 0) + if debug or verbose: + print(f"[vm.ffi] {cmd}") + + process = Popen(cmd, stdout=PIPE, stderr=PIPE) + (stdout, stderr) = process.communicate() + + if stderr: + stderr_str = stderr.decode("utf-8") + print(f"[vm.ffi] {cmd}, stderr: {red(stderr_str)}") + + out_str = stdout.decode("utf-8").strip() + + if debug: + print(f"[vm.ffi] {cmd}, stdout: {green(out_str)}") + + if decode_hex(out_str) is not None: + # encode hex strings as is for compatibility with foundry's ffi + pass + else: + # encode non-hex strings as hex + out_str = out_str.encode("utf-8").hex() + + return stringified_bytes_to_bytes(out_str) + + else: + # TODO: support other cheat codes + msg = f"Unsupported cheat code: calldata = {hexify(arg)}" + raise HalmosException(msg) diff --git a/src/halmos/console.py b/src/halmos/console.py new file mode 100644 index 00000000..1fa0ed27 --- /dev/null +++ b/src/halmos/console.py @@ -0,0 +1,111 @@ +from z3 import BitVecRef + +from .utils import * + + +def log_uint256(arg: BitVec) -> None: + b = extract_bytes(arg, 4, 32) + console.log(render_uint(b)) + + +def log_string(arg: BitVec) -> None: + str_val = extract_string_argument(arg, 0) + console.log(str_val) + + +def log_bytes(arg: BitVec) -> None: + b = extract_bytes_argument(arg, 0) + console.log(render_bytes(b)) + + +def log_string_address(arg: BitVec) -> None: + str_val = extract_string_argument(arg, 0) + addr = extract_bytes(arg, 36, 32) + console.log(f"{str_val} {render_address(addr)}") + + +def log_address(arg: BitVec) -> None: + addr = extract_bytes(arg, 4, 32) + console.log(render_address(addr)) + + +def log_string_bool(arg: BitVec) -> None: + str_val = extract_string_argument(arg, 0) + bool_val = extract_bytes(arg, 36, 32) + console.log(f"{str_val} {render_bool(bool_val)}") + + +def log_bool(arg: BitVec) -> None: + bool_val = extract_bytes(arg, 4, 32) + console.log(render_bool(bool_val)) + + +def log_string_string(arg: BitVec) -> None: + str1_val = extract_string_argument(arg, 0) + str2_val = extract_string_argument(arg, 1) + console.log(f"{str1_val} {str2_val}") + + +def log_bytes32(arg: BitVec) -> None: + b = extract_bytes(arg, 4, 32) + console.log(hexify(b)) + + +def log_string_int256(arg: BitVec) -> None: + str_val = extract_string_argument(arg, 0) + int_val = extract_bytes(arg, 36, 32) + console.log(f"{str_val} {render_int(int_val)}") + + +def log_int256(arg: BitVec) -> None: + int_val = extract_bytes(arg, 4, 32) + console.log(render_int(int_val)) + + +def log_string_uint256(arg: BitVec) -> None: + str_val = extract_string_argument(arg, 0) + uint_val = extract_bytes(arg, 36, 32) + console.log(f"{str_val} {render_uint(uint_val)}") + + +class console: + # see forge-std/console2.sol + address: BitVecRef = con_addr(0x000000000000000000636F6E736F6C652E6C6F67) + + handlers = { + 0xF82C50F1: log_uint256, + 0xF5B1BBA9: log_uint256, # alias for 'log(uint)' + 0x41304FAC: log_string, + 0x0BE77F56: log_bytes, + 0x319AF333: log_string_address, + 0x2C2ECBC2: log_address, + 0xC3B55635: log_string_bool, + 0x32458EED: log_bool, + 0x4B5C4277: log_string_string, + 0x27B7CF85: log_bytes32, + 0x3CA6268E: log_string_int256, + 0x2D5B6CB9: log_int256, + 0xB60E72CC: log_string_uint256, + } + + @staticmethod + def log(what: str) -> None: + print(f"[console.log] {magenta(what)}") + + @staticmethod + def handle(ex, arg: BitVec) -> None: + try: + funsig: int = int_of( + extract_funsig(arg), "symbolic console function selector" + ) + + if handler := console.handlers.get(funsig): + return handler(arg) + + info( + f"Unsupported console function: selector = 0x{funsig:0>8x}, " + f"calldata = {hexify(arg)}" + ) + except Exception as e: + # we don't want to fail execution because of an issue during console.log + warn(f"console.handle: {repr(e)} with arg={hexify(arg)}") diff --git a/src/halmos/exceptions.py b/src/halmos/exceptions.py new file mode 100644 index 00000000..a8982f6e --- /dev/null +++ b/src/halmos/exceptions.py @@ -0,0 +1,148 @@ +""" +Ethereum Virtual Machine (EVM) Exceptions +======================================== + +Exceptions thrown during EVM execution. + +Note: this is a modified version execution-specs' src/ethereum//vm/exceptions.pyd +""" + + +class HalmosException(Exception): + pass + + +class NotConcreteError(HalmosException): + pass + + +class EvmException(Exception): + """ + Base class for all EVM exceptions. + """ + + pass + + +class Revert(EvmException): + """ + Raised by the `REVERT` opcode. + + Unlike other EVM exceptions this does not result in the consumption of all gas. + """ + + pass + + +class ExceptionalHalt(EvmException): + """ + Indicates that the EVM has experienced an exceptional halt. This causes + execution to immediately end with all gas being consumed. + """ + + pass + + +class StackUnderflowError(ExceptionalHalt): + """ + Occurs when a pop is executed on an empty stack. + """ + + pass + + +class StackOverflowError(ExceptionalHalt): + """ + Occurs when a push is executed on a stack at max capacity. + """ + + pass + + +class OutOfGasError(ExceptionalHalt): + """ + Occurs when an operation costs more than the amount of gas left in the + frame. + """ + + pass + + +class InvalidOpcode(ExceptionalHalt): + """ + Raised when an invalid opcode is encountered. + """ + + pass + + +class InvalidJumpDestError(ExceptionalHalt): + """ + Occurs when the destination of a jump operation doesn't meet any of the + following criteria: + + * The jump destination is less than the length of the code. + * The jump destination should have the `JUMPDEST` opcode (0x5B). + * The jump destination shouldn't be part of the data corresponding to + `PUSH-N` opcodes. + """ + + pass + + +class MessageDepthLimitError(ExceptionalHalt): + """ + Raised when the message depth is greater than `1024` + """ + + pass + + +class WriteInStaticContext(ExceptionalHalt): + """ + Raised when an attempt is made to modify the state while operating inside + of a STATICCALL context. + """ + + pass + + +class OutOfBoundsRead(ExceptionalHalt): + """ + Raised when an attempt was made to read data beyond the + boundaries of the buffer. + """ + + pass + + +class InvalidParameter(ExceptionalHalt): + """ + Raised when invalid parameters are passed. + """ + + pass + + +class InvalidContractPrefix(ExceptionalHalt): + """ + Raised when the new contract code starts with 0xEF. + """ + + pass + + +class FailCheatcode(ExceptionalHalt): + """ + Raised when invoking hevm's vm.fail() cheatcode + """ + + pass + + +class AddressCollision(ExceptionalHalt): + """ + Raised when trying to deploy into a non-empty address + """ + + pass diff --git a/src/halmos/parser.py b/src/halmos/parser.py new file mode 100644 index 00000000..db23075b --- /dev/null +++ b/src/halmos/parser.py @@ -0,0 +1,269 @@ +# SPDX-License-Identifier: AGPL-3.0 + +import os +import argparse + + +def mk_arg_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="halmos", epilog="For more information, see https://github.com/a16z/halmos" + ) + + parser.add_argument( + "--root", + metavar="DIRECTORY", + default=os.getcwd(), + help="source root directory (default: current directory)", + ) + parser.add_argument( + "--contract", + metavar="CONTRACT_NAME", + help="run tests in the given contract. Shortcut for `--match-contract '^{NAME}$'`.", + ) + parser.add_argument( + "--match-contract", + "--mc", + metavar="CONTRACT_NAME_REGEX", + default="", + help="run tests in contracts matching the given regex. Ignored if the --contract name is given. (default: '%(default)s')", + ) + parser.add_argument( + "--function", + metavar="FUNCTION_NAME_PREFIX", + default="check_", + help="run tests matching the given prefix. Shortcut for `--match-test '^{PREFIX}'`. (default: '%(default)s')", + ) + parser.add_argument( + "--match-test", + "--mt", + metavar="FUNCTION_NAME_REGEX", + default="", + help="run tests matching the given regex. The --function prefix is automatically added, unless the regex starts with '^'. (default: '%(default)s')", + ) + + parser.add_argument( + "--loop", + metavar="MAX_BOUND", + type=int, + default=2, + help="set loop unrolling bounds (default: %(default)s)", + ) + parser.add_argument( + "--width", + metavar="MAX_WIDTH", + type=int, + default=2**64, + help="set the max number of paths (default: %(default)s)", + ) + parser.add_argument( + "--depth", metavar="MAX_DEPTH", type=int, help="set the max path length" + ) + parser.add_argument( + "--array-lengths", + metavar="NAME1=LENGTH1,NAME2=LENGTH2,...", + help="set the length of dynamic-sized arrays including bytes and string (default: loop unrolling bound)", + ) + + parser.add_argument( + "--uninterpreted-unknown-calls", + metavar="SELECTOR1,SELECTOR2,...", + # onERC721Received, IERC1271.isValidSignature + default="0x150b7a02,0x1626ba7e", + help="use uninterpreted abstractions for unknown external calls with the given function signatures (default: '%(default)s')", + ) + parser.add_argument( + "--return-size-of-unknown-calls", + metavar="BYTE_SIZE", + type=int, + default=32, + help="set the byte size of return data from uninterpreted unknown external calls (default: %(default)s)", + ) + + parser.add_argument( + "--storage-layout", + choices=["solidity", "generic"], + default="solidity", + help="Select one of the available storage layout models. The generic model should only be necessary for vyper, huff, or unconventional storage patterns in yul.", + ) + + parser.add_argument( + "--symbolic-storage", + action="store_true", + help="set default storage values to symbolic", + ) + parser.add_argument( + "--symbolic-msg-sender", action="store_true", help="set msg.sender symbolic" + ) + parser.add_argument( + "--no-test-constructor", + action="store_true", + help="do not run the constructor of test contracts", + ) + + parser.add_argument( + "--ffi", + action="store_true", + help="allow the usage of FFI to call external functions", + ) + + parser.add_argument( + "--version", action="store_true", help="print the version number" + ) + + # debugging options + group_debug = parser.add_argument_group("Debugging options") + + group_debug.add_argument( + "-v", + "--verbose", + action="count", + default=0, + help="increase verbosity levels: -v, -vv, -vvv, ...", + ) + group_debug.add_argument( + "-st", "--statistics", action="store_true", help="print statistics" + ) + group_debug.add_argument("--debug", action="store_true", help="run in debug mode") + group_debug.add_argument( + "--log", metavar="LOG_FILE_PATH", help="log every execution steps in JSON" + ) + group_debug.add_argument( + "--json-output", metavar="JSON_FILE_PATH", help="output test results in JSON" + ) + group_debug.add_argument( + "--minimal-json-output", + action="store_true", + help="include minimal information in the JSON output", + ) + group_debug.add_argument( + "--print-steps", action="store_true", help="print every execution steps" + ) + group_debug.add_argument( + "--print-states", action="store_true", help="print all final execution states" + ) + group_debug.add_argument( + "--print-failed-states", + action="store_true", + help="print failed execution states", + ) + group_debug.add_argument( + "--print-blocked-states", + action="store_true", + help="print blocked execution states", + ) + group_debug.add_argument( + "--print-setup-states", action="store_true", help="print setup execution states" + ) + group_debug.add_argument( + "--print-full-model", + action="store_true", + help="print full counterexample model", + ) + group_debug.add_argument( + "--early-exit", + action="store_true", + help="stop after a counterexample is found", + ) + + group_debug.add_argument( + "--dump-smt-queries", + action="store_true", + help="dump SMT queries for assertion violations", + ) + + # build options + group_build = parser.add_argument_group("Build options") + + group_build.add_argument( + "--forge-build-out", + metavar="DIRECTORY_NAME", + default="out", + help="forge build artifacts directory name (default: '%(default)s')", + ) + + # smt solver options + group_solver = parser.add_argument_group("Solver options") + + group_solver.add_argument( + "--smt-exp-by-const", + metavar="N", + type=int, + default=2, + help="interpret constant power up to N (default: %(default)s)", + ) + + group_solver.add_argument( + "--solver-timeout-branching", + metavar="TIMEOUT", + type=int, + default=1, + help="set timeout (in milliseconds) for solving branching conditions; 0 means no timeout (default: %(default)s)", + ) + group_solver.add_argument( + "--solver-timeout-assertion", + metavar="TIMEOUT", + type=int, + default=1000, + help="set timeout (in milliseconds) for solving assertion violation conditions; 0 means no timeout (default: %(default)s)", + ) + group_solver.add_argument( + "--solver-max-memory", + metavar="SIZE", + type=int, + default=0, + help="set memory limit (in megabytes) for the solver; 0 means no limit (default: %(default)s)", + ) + group_solver.add_argument( + "--solver-fresh", + action="store_true", + help="run an extra solver with a fresh state for unknown", + ) + group_solver.add_argument( + "--solver-subprocess", + action="store_true", + help="run an extra solver in subprocess for unknown", + ) + group_solver.add_argument( + "--solver-subprocess-command", + metavar="COMMAND", + default="z3 -model", + help="use the given command for the subprocess solver (requires --solver-subprocess) (default: '%(default)s')", + ) + group_solver.add_argument( + "--solver-parallel", + action="store_true", + help="run assertion solvers in parallel", + ) + group_solver.add_argument( + "--solver-threads", + metavar="N", + type=int, + # the default value of max_workers for ThreadPoolExecutor + # TODO: set default value := total physical memory size / average z3 memory footprint + default=min(32, os.cpu_count() + 4), + help=f"set the number of threads for parallel solvers (default: %(default)s)", + ) + + # internal options + group_internal = parser.add_argument_group("Internal options") + + group_internal.add_argument( + "--bytecode", metavar="HEX_STRING", help="execute the given bytecode" + ) + group_internal.add_argument( + "--reset-bytecode", + metavar="ADDR1=CODE1,ADDR2=CODE2,...", + help="reset the bytecode of given addresses after setUp()", + ) + group_internal.add_argument( + "--test-parallel", action="store_true", help="run tests in parallel" + ) + + # experimental options + group_experimental = parser.add_argument_group("Experimental options") + + group_experimental.add_argument( + "--symbolic-jump", action="store_true", help="support symbolic jump destination" + ) + + return parser diff --git a/src/halmos/pools.py b/src/halmos/pools.py index e0983edf..12d1f44f 100644 --- a/src/halmos/pools.py +++ b/src/halmos/pools.py @@ -1,5 +1,5 @@ from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor # defined as globals so that they can be imported from anywhere but instantiated only once -process_pool = ProcessPoolExecutor() -thread_pool = ThreadPoolExecutor() +# process_pool = ProcessPoolExecutor() +# thread_pool = ThreadPoolExecutor() diff --git a/src/halmos/sevm.py b/src/halmos/sevm.py index f44d80e7..3b23dc85 100644 --- a/src/halmos/sevm.py +++ b/src/halmos/sevm.py @@ -1,84 +1,80 @@ # SPDX-License-Identifier: AGPL-3.0 -import json import math import re from copy import deepcopy from collections import defaultdict -from typing import List, Dict, Union as UnionType, Tuple, Any, Optional -from functools import reduce - +from dataclasses import dataclass, field +from functools import reduce, lru_cache +from typing import ( + Any, + Callable, + Dict, + Iterator, + List, + Optional, + Set, + Tuple, + Type, + TypeVar, + Union as UnionType, +) from z3 import * -from .utils import ( - EVM, - sha3_inv, - restore_precomputed_hashes, - str_opcode, - assert_address, - assert_uint256, - con_addr, - bv_value_to_bytes, - hexify, + +from .cheatcodes import halmos_cheat_code, hevm_cheat_code, Prank +from .console import console +from .exceptions import * +from .utils import * +from .warnings import ( + warn, + LIBRARY_PLACEHOLDER, + INTERNAL_ERROR, ) -from .cheatcodes import halmos_cheat_code, hevm_cheat_code, console, Prank -Word = Any # z3 expression (including constants) -Byte = Any # z3 expression (including constants) -Bytes = Any # z3 expression (including constants) -Address = BitVecRef # 160-bitvector Steps = Dict[int, Dict[str, Any]] # execution tree +EMPTY_BYTES = b"" +MAX_CALL_DEPTH = 1024 + +# TODO: make this configurable +MAX_MEMORY_SIZE = 2**20 + # symbolic states # calldataload(index) -f_calldataload = Function("calldataload", BitVecSort(256), BitVecSort(256)) +f_calldataload = Function("calldataload", BitVecSort256, BitVecSort256) # calldatasize() -f_calldatasize = Function("calldatasize", BitVecSort(256)) +f_calldatasize = Function("calldatasize", BitVecSort256) # extcodesize(target address) -f_extcodesize = Function("extcodesize", BitVecSort(160), BitVecSort(256)) +f_extcodesize = Function("extcodesize", BitVecSort160, BitVecSort256) # extcodehash(target address) -f_extcodehash = Function("extcodehash", BitVecSort(160), BitVecSort(256)) +f_extcodehash = Function("extcodehash", BitVecSort160, BitVecSort256) # blockhash(block number) -f_blockhash = Function("blockhash", BitVecSort(256), BitVecSort(256)) +f_blockhash = Function("blockhash", BitVecSort256, BitVecSort256) # gas(cnt) -f_gas = Function("gas", BitVecSort(256), BitVecSort(256)) +f_gas = Function("gas", BitVecSort256, BitVecSort256) # gasprice() -f_gasprice = Function("gasprice", BitVecSort(256)) +f_gasprice = Function("gasprice", BitVecSort256) # origin() -f_origin = Function("origin", BitVecSort(160)) +f_origin = Function("origin", BitVecSort160) # uninterpreted arithmetic -f_add = { - 256: Function("evm_bvadd", BitVecSort(256), BitVecSort(256), BitVecSort(256)), - 264: Function("evm_bvadd_264", BitVecSort(264), BitVecSort(264), BitVecSort(264)), -} -f_sub = Function("evm_bvsub", BitVecSort(256), BitVecSort(256), BitVecSort(256)) -f_mul = { - 256: Function("evm_bvmul", BitVecSort(256), BitVecSort(256), BitVecSort(256)), - 512: Function("evm_bvmul_512", BitVecSort(512), BitVecSort(512), BitVecSort(512)), -} -f_div = Function("evm_bvudiv", BitVecSort(256), BitVecSort(256), BitVecSort(256)) +f_div = Function("evm_bvudiv", BitVecSort256, BitVecSort256, BitVecSort256) f_mod = { - 256: Function("evm_bvurem", BitVecSort(256), BitVecSort(256), BitVecSort(256)), - 264: Function("evm_bvurem_264", BitVecSort(264), BitVecSort(264), BitVecSort(264)), - 512: Function("evm_bvurem_512", BitVecSort(512), BitVecSort(512), BitVecSort(512)), + 256: Function("evm_bvurem", BitVecSort256, BitVecSort256, BitVecSort256), + 264: Function("evm_bvurem_264", BitVecSort264, BitVecSort264, BitVecSort264), + 512: Function("evm_bvurem_512", BitVecSort512, BitVecSort512, BitVecSort512), } -f_sdiv = Function("evm_bvsdiv", BitVecSort(256), BitVecSort(256), BitVecSort(256)) -f_smod = Function("evm_bvsrem", BitVecSort(256), BitVecSort(256), BitVecSort(256)) -f_exp = Function("evm_exp", BitVecSort(256), BitVecSort(256), BitVecSort(256)) +f_sdiv = Function("evm_bvsdiv", BitVecSort256, BitVecSort256, BitVecSort256) +f_smod = Function("evm_bvsrem", BitVecSort256, BitVecSort256, BitVecSort256) +f_exp = Function("evm_exp", BitVecSort256, BitVecSort256, BitVecSort256) magic_address: int = 0xAAAA0000 -new_address_offset: int = 1 - - -def id_str(x: Any) -> str: - return hexify(x).replace(" ", "") - +create2_magic_address: int = 0xBBBB0000 -def name_of(x: str) -> str: - return re.sub(r"\s+", "_", x) +new_address_offset: int = 1 class Instruction: @@ -97,9 +93,7 @@ def __str__(self) -> str: if self.operand is not None: operand = self.operand if isinstance(operand, bytes): - operand = BitVecVal( - int.from_bytes(self.operand, "big"), len(self.operand) * 8 - ) + operand = con(int.from_bytes(operand, "big"), len(operand) * 8) expected_operand_length = instruction_length(self.opcode) - 1 actual_operand_length = operand.size() // 8 @@ -117,28 +111,19 @@ def __len__(self) -> int: return instruction_length(self.opcode) -class NotConcreteError(Exception): - pass - - -def unbox_int(x: Any) -> Any: - """Convert int-like objects to int""" - if isinstance(x, bytes): - return int.from_bytes(x, "big") - - if is_bv_value(x): - return x.as_long() - - return x - +def id_str(x: Any) -> str: + return hexify(x).replace(" ", "") -def int_of(x: Any, err: str = "expected concrete value but got") -> int: - res = unbox_int(x) - if isinstance(res, int): - return res +def padded_slice(lst: List, start: int, size: int, default=0) -> List: + """ + Return a slice of lst, starting at start and with size elements. If the slice + is out of bounds, pad with default. + """ - raise NotConcreteError(f"{err}: {x}") + end = start + size + n = len(lst) + return [lst[i] if i < n else default for i in range(start, end)] def iter_bytes(x: Any, _byte_length: int = -1): @@ -180,56 +165,16 @@ def mnemonic(opcode) -> str: return str(opcode) -def concat(args): - if len(args) > 1: - return Concat(args) - else: - return args[0] - - -def uint256(x: BitVecRef) -> BitVecRef: - bitsize = x.size() - if bitsize > 256: - raise ValueError(x) - if bitsize == 256: - return x - return simplify(ZeroExt(256 - bitsize, x)) - - -def uint160(x: BitVecRef) -> BitVecRef: - bitsize = x.size() - if bitsize > 256: - raise ValueError(x) - if bitsize == 160: - return x - if bitsize > 160: - return simplify(Extract(159, 0, x)) - else: - return simplify(ZeroExt(160 - bitsize, x)) - - -def con(n: int, size_bits=256) -> Word: - return BitVecVal(n, size_bits) - - -def byte_length(x: Any) -> int: - if is_bv(x): - if x.size() % 8 != 0: - raise ValueError(x) - return x.size() >> 3 - - if isinstance(x, bytes): - return len(x) - - raise ValueError(x) - - def instruction_length(opcode: Any) -> int: opcode = int_of(opcode) return (opcode - EVM.PUSH0 + 1) if EVM.PUSH1 <= opcode <= EVM.PUSH32 else 1 def wextend(mem: List[UnionType[int, BitVecRef]], loc: int, size: int) -> None: + new_msize = loc + size + if new_msize > MAX_MEMORY_SIZE: + raise OutOfGasError(f"memory offset {new_msize} exceeds max memory") + mem.extend([0] * (loc + size - len(mem))) @@ -262,16 +207,16 @@ def wload( # wrap concrete bytes in BitVecs # this would truncate the upper bits if the value didn't fit in 8 bits # therefore we rely on the value range check above to raise an error - wrapped = [BitVecVal(i, 8) if not is_bv(i) else i for i in memslice] + wrapped = [BitVecVal(i, BitVecSort8) if not is_bv(i) else i for i in memslice] - # BitVecSort(size * 8) + # BitVecSorts[size * 8] return simplify(concat(wrapped)) def wstore( mem: List[UnionType[int, BitVecRef]], loc: int, size: int, val: Bytes ) -> None: - if not eq(val.sort(), BitVecSort(size * 8)): + if not eq(val.sort(), BitVecSorts[size * 8]): raise ValueError(val) wextend(mem, loc, size) for i in range(size): @@ -292,7 +237,7 @@ def wstore_partial( return if not datasize >= offset + size: - raise ValueError(datasize, offset, size) + raise OutOfBoundsRead(datasize, offset, size) if is_bv(data): sub_data = Extract( @@ -313,58 +258,166 @@ def wstore_bytes( raise ValueError(size, arr) wextend(mem, loc, size) for i in range(size): - if not eq(arr[i].sort(), BitVecSort(8)): - raise ValueError(arr) - mem[loc + i] = arr[i] + val = arr[i] + if not is_byte(val): + raise ValueError(val) + mem[loc + i] = val -def extract_bytes(data: BitVecRef, byte_offset: int, size_bytes: int) -> BitVecRef: - """Extract bytes from calldata. Zero-pad if out of bounds.""" - n = data.size() - if n % 8 != 0: - raise ValueError(n) - # will extract hi - lo + 1 bits - hi = n - 1 - byte_offset * 8 - lo = n - byte_offset * 8 - size_bytes * 8 - lo = 0 if lo < 0 else lo +def is_byte(x: Any) -> bool: + if is_bv(x): + return eq(x.sort(), BitVecSort8) + elif isinstance(x, int): + return 0 <= x < 256 + else: + return False - val = simplify(Extract(hi, lo, data)) - zero_padding = size_bytes * 8 - val.size() - if zero_padding < 0: - raise ValueError(val) - if zero_padding > 0: - val = simplify(Concat(val, con(0, zero_padding))) - - return val - - -def extract_funsig(calldata: BitVecRef): - """Extracts the function signature (first 4 bytes) from calldata""" - n = calldata.size() - # return simplify(Extract(n-1, n-32, calldata)) - return extract_bytes(calldata, 0, 4) - - -def extract_string_argument(calldata: BitVecRef, arg_idx: int): - """Extracts idx-th argument of string from calldata""" - string_offset = int_of( - extract_bytes(calldata, 4 + arg_idx * 32, 32), - "symbolic offset for string argument", - ) - string_length = int_of( - extract_bytes(calldata, 4 + string_offset, 32), - "symbolic size for string argument", - ) - if string_length == 0: - return "" - string_value = int_of( - extract_bytes(calldata, 4 + string_offset + 32, string_length), - "symbolic string argument", - ) - string_bytes = string_value.to_bytes(string_length, "big") - return string_bytes.decode("utf-8") +def normalize(expr: Any) -> Any: + # Concat(Extract(255, 8, op(x, y)), op(Extract(7, 0, x), Extract(7, 0, y))) => op(x, y) + def normalize_extract(arg0, arg1): + if ( + arg0.decl().name() == "extract" + and arg0.num_args() == 1 + and arg0.params() == [255, 8] + ): + target = arg0.arg(0) # op(x, y) + + # this form triggers the partial inward-propagation of extracts in simplify() + # that is, `Extract(7, 0, op(x, y))` => `op(Extract(7, 0, x), Extract(7, 0, y))`, followed by further simplification + target_equivalent = Concat(Extract(255, 8, target), Extract(7, 0, target)) + + given = Concat(arg0, arg1) + + # since target_equivalent and given may not be structurally equal, we compare their fully simplified forms + if eq(simplify(given), simplify(target_equivalent)): + # here we have: given == target_equivalent == target + return target + + return None + + if expr.decl().name() == "concat" and expr.num_args() >= 2: + new_args = [] + + i = 0 + n = expr.num_args() + + # apply normalize_extract for each pair of adjacent arguments + while i < n - 1: + arg0 = expr.arg(i) + arg1 = expr.arg(i + 1) + + arg0_arg1 = normalize_extract(arg0, arg1) + + if arg0_arg1 is None: # not simplified + new_args.append(arg0) + i += 1 + else: # simplified into a single term + new_args.append(arg0_arg1) + i += 2 + + # handle the last element + if i == n - 1: + new_args.append(expr.arg(i)) + + return concat(new_args) + + return expr + + +@dataclass(frozen=True) +class EventLog: + """ + Data record produced during the execution of a transaction. + """ + + address: Address + topics: List[Word] + data: Optional[Bytes] + + +@dataclass(frozen=True) +class Message: + target: Address + caller: Address + value: Word + data: List[Byte] + is_static: bool = False + call_scheme: int = EVM.CALL + gas: Optional[Word] = None + + def is_create(self) -> bool: + return self.call_scheme == EVM.CREATE or self.call_scheme == EVM.CREATE2 + + +@dataclass +class CallOutput: + """ + Data record produced during the execution of a call. + """ + + data: Optional[Bytes] = None + accounts_to_delete: Set[Address] = field(default_factory=set) + error: Optional[UnionType[EvmException, HalmosException]] = None + return_scheme: Optional[int] = None + + # TODO: + # - touched_accounts + # not modeled: + # - gas_refund + # - gas_left + + +TraceElement = UnionType["CallContext", EventLog] + + +@dataclass +class CallContext: + message: Message + output: CallOutput = field(default_factory=CallOutput) + depth: int = 1 + trace: List[TraceElement] = field(default_factory=list) + + def subcalls(self) -> Iterator["CallContext"]: + return iter(t for t in self.trace if isinstance(t, CallContext)) + + def last_subcall(self) -> Optional["CallContext"]: + """ + Returns the last subcall or None if there are no subcalls. + """ + + for c in reversed(self.trace): + if isinstance(c, CallContext): + return c + + return None + + def logs(self) -> Iterator[EventLog]: + return iter(t for t in self.trace if isinstance(t, EventLog)) + + def is_stuck(self) -> bool: + """ + When called after execution, this method returns True if the call is stuck, + i.e. it encountered an internal error and has no output. + + This is meaningless during execution, because the call may not yet have an output + """ + return self.output.data is None + + def get_stuck_reason(self) -> Optional[HalmosException]: + """ + Returns the first internal error encountered during the execution of the call. + """ + if isinstance(self.output.error, HalmosException): + return self.output.error + + if self.output.data is not None: + # if this context has output data (including empty bytes), it is not stuck + return None + + if (last_subcall := self.last_subcall()) is not None: + return last_subcall.get_stuck_reason() class State: @@ -377,14 +430,14 @@ def __init__(self) -> None: def __deepcopy__(self, memo): # -> State: st = State() - st.stack = deepcopy(self.stack) - st.memory = deepcopy(self.memory) + st.stack = self.stack.copy() + st.memory = self.memory.copy() return st def __str__(self) -> str: return "".join( [ - f"Stack: {str(self.stack)}\n", + f"Stack: {str(list(reversed(self.stack)))}\n", # self.str_memory(), ] ) @@ -399,22 +452,18 @@ def str_memory(self) -> str: return ret + "\n" def push(self, v: Word) -> None: - if not (eq(v.sort(), BitVecSort(256)) or is_bool(v)): + if not (eq(v.sort(), BitVecSort256) or is_bool(v)): raise ValueError(v) - self.stack.insert(0, simplify(v)) + self.stack.append(simplify(v)) def pop(self) -> Word: - v = self.stack[0] - del self.stack[0] - return v + return self.stack.pop() def dup(self, n: int) -> None: - self.push(self.stack[n - 1]) + self.push(self.stack[-n]) def swap(self, n: int) -> None: - tmp = self.stack[0] - self.stack[0] = self.stack[n] - self.stack[n] = tmp + self.stack[-(n + 1)], self.stack[-1] = self.stack[-1], self.stack[-(n + 1)] def mloc(self) -> int: loc: int = int_of(self.pop(), "symbolic memory offset") @@ -437,10 +486,7 @@ def mload(self) -> None: def ret(self) -> Bytes: loc: int = self.mloc() size: int = int_of(self.pop(), "symbolic return data size") # size in bytes - if size > 0: - return wload(self.memory, loc, size, prefer_concrete=True) - else: - return None + return wload(self.memory, loc, size, prefer_concrete=True) if size else b"" class Block: @@ -472,13 +518,23 @@ class Contract: # (typically a Concat() of concrete and symbolic values) _rawcode: UnionType[bytes, BitVecRef] - def __init__(self, rawcode: UnionType[bytes, BitVecRef]) -> None: + def __init__(self, rawcode: UnionType[bytes, BitVecRef, str]) -> None: + if rawcode is None: + raise HalmosException("invalid contract code: None") + if is_bv_value(rawcode): if rawcode.size() % 8 != 0: raise ValueError(rawcode) rawcode = rawcode.as_long().to_bytes(rawcode.size() // 8, "big") + + if isinstance(rawcode, str): + rawcode = bytes.fromhex(rawcode) + self._rawcode = rawcode + # cache length since it doesn't change after initialization + self._length = byte_length(rawcode) + def __init_jumpdests(self): self.jumpdests = set() @@ -497,16 +553,19 @@ def from_hexcode(hexcode: str): if len(hexcode) % 2 != 0: raise ValueError(hexcode) - if hexcode.startswith("0x"): - hexcode = hexcode[2:] + if "__" in hexcode: + warn(LIBRARY_PLACEHOLDER, f"contract hexcode contains library placeholder") - return Contract(bytes.fromhex(hexcode)) + try: + return Contract(bytes.fromhex(stripped(hexcode))) + except ValueError as e: + raise ValueError(f"{e} (hexcode={hexcode})") def decode_instruction(self, pc: int) -> Instruction: - opcode = int_of(self[pc]) + opcode = int_of(self[pc], f"symbolic opcode at pc={pc}") if EVM.PUSH1 <= opcode <= EVM.PUSH32: - operand = self[pc + 1 : pc + opcode - EVM.PUSH0 + 1] + operand = self.slice(pc + 1, pc + opcode - EVM.PUSH0 + 1) return Instruction(opcode, pc=pc, operand=operand) return Instruction(opcode, pc=pc) @@ -515,16 +574,10 @@ def next_pc(self, pc): opcode = self[pc] return pc + instruction_length(opcode) - def __getslice__(self, slice): - step = 1 if slice.step is None else slice.step - if step != 1: - return ValueError(f"slice step must be 1 but got {slice}") - + def slice(self, start, stop) -> UnionType[bytes, BitVecRef]: # symbolic if is_bv(self._rawcode): - extracted = extract_bytes( - self._rawcode, slice.start, slice.stop - slice.start - ) + extracted = extract_bytes(self._rawcode, start, stop - start) # check if that part of the code is concrete if is_bv_value(extracted): @@ -534,14 +587,14 @@ def __getslice__(self, slice): return extracted # concrete - return self._rawcode[slice.start : slice.stop] + size = stop - start + data = padded_slice(self._rawcode, start, size, default=0) + return bytes(data) - def __getitem__(self, key) -> UnionType[int, BitVecRef]: + @lru_cache(maxsize=256) + def __getitem__(self, key: int) -> UnionType[int, BitVecRef]: """Returns the byte at the given offset.""" - if isinstance(key, slice): - return self.__getslice__(key) - - offset = int_of(key, "symbolic index into contract bytecode") + offset = int_of(key, "symbolic index into contract bytecode {offset!r}") # support for negative indexing, e.g. contract[-1] if offset < 0: @@ -563,7 +616,7 @@ def __getitem__(self, key) -> UnionType[int, BitVecRef]: def __len__(self) -> int: """Returns the length of the bytecode in bytes.""" - return byte_length(self._rawcode) + return self._length def valid_jump_destinations(self) -> set: """Returns the set of valid jump destinations.""" @@ -587,43 +640,134 @@ def __next__(self) -> Instruction: if self.pc >= len(self.contract): raise StopIteration + if self.is_symbolic and is_bv(self.contract[self.pc]): + raise StopIteration + insn = self.contract.decode_instruction(self.pc) self.pc += len(insn) return insn +class Path: + # a Path object represents a prefix of the path currently being executed + # initially, it's an empty path at the beginning of execution + + solver: Solver + num_scopes: int + # path constraints include both explicit branching conditions and implicit assumptions (eg, no hash collisions) + # TODO: separate these two types of constraints, so that we can display only branching conditions to users + conditions: List + branching: List # indexes of conditions + pending: List + + def __init__(self, solver: Solver): + self.solver = solver + self.num_scopes = 0 + self.conditions = [] + self.branching = [] + self.pending = [] + + def __deepcopy__(self, memo): + raise NotImplementedError(f"use the branch() method instead of deepcopy()") + + def __str__(self) -> str: + branching_conds = [self.conditions[idx] for idx in self.branching] + return "".join( + [f"- {cond}\n" for cond in branching_conds if str(cond) != "True"] + ) + + def branch(self, cond): + if len(self.pending) > 0: + raise ValueError("branching from an inactive path", self) + + # create a new path that shares the same solver instance to minimize memory usage + # note: sharing the solver instance complicates the use of randomized path exploration approaches, which can be more efficient for quickly finding bugs. + # currently, a dfs-based path exploration is employed, which is suitable for scenarios where exploring all paths is necessary, e.g., when proving the absence of bugs. + path = Path(self.solver) + + # create a new scope within the solver, and save the current scope + # the solver will roll back to this scope later when the new path is activated + path.num_scopes = self.solver.num_scopes() + self.solver.push() + + # shallow copy because existing conditions won't change + # note: deep copy would be needed later for advanced query optimizations (eg, constant propagation) + path.conditions = self.conditions.copy() + + # store the branching condition aside until the new path is activated. + path.pending.append(cond) + + return path + + def is_activated(self) -> bool: + return len(self.pending) == 0 + + def activate(self): + if self.solver.num_scopes() < self.num_scopes: + raise ValueError( + "invalid num_scopes", self.solver.num_scopes(), self.num_scopes + ) + + self.solver.pop(self.solver.num_scopes() - self.num_scopes) + + self.extend(self.pending, branching=True) + self.pending = [] + + def append(self, cond, branching=False): + cond = simplify(cond) + + if is_true(cond): + return + + self.solver.add(cond) + self.conditions.append(cond) + + if branching: + self.branching.append(len(self.conditions) - 1) + + def extend(self, conds, branching=False): + for cond in conds: + self.append(cond, branching=branching) + + def extend_path(self, path): + # branching conditions are not preserved + self.extend(path.conditions) + + class Exec: # an execution path # network code: Dict[Address, Contract] storage: Dict[Address, Dict[int, Any]] # address -> { storage slot -> value } balance: Any # address -> balance + # block block: Block + # tx - calldata: List[Byte] # msg.data - callvalue: Word # msg.value - caller: Address # msg.sender - this: Address # current account address + context: CallContext + callback: Optional[Callable] # to be called when returning back to parent context + # vm state + this: Address # current account address + pgm: Contract pc: int st: State # stack and memory jumpis: Dict[str, Dict[bool, int]] # for loop detection - output: Any # returndata symbolic: bool # symbolic or concrete storage prank: Prank + addresses_to_delete: Set[Address] + # path - solver: Solver - path: List[Any] # path conditions - # logs - log: List[Tuple[List[Word], Any]] # event logs emitted + path: Path # path conditions + alias: Dict[Address, Address] # address aliases + + # internal bookkeeping cnts: Dict[str, Dict[int, int]] # opcode -> frequency; counters - sha3s: List[Tuple[Word, Word]] # sha3 hashes generated + sha3s: Dict[Word, int] # sha3 hashes generated storages: Dict[Any, Any] # storage updates balances: Dict[Any, Any] # balance updates calls: List[Any] # external calls - failed: bool - error: str def __init__(self, **kwargs) -> None: self.code = kwargs["code"] @@ -632,38 +776,78 @@ def __init__(self, **kwargs) -> None: # self.block = kwargs["block"] # - self.calldata = kwargs["calldata"] - self.callvalue = kwargs["callvalue"] - self.caller = kwargs["caller"] - self.this = kwargs["this"] + self.context = kwargs["context"] + self.callback = kwargs["callback"] # + self.this = kwargs["this"] + self.pgm = kwargs["pgm"] self.pc = kwargs["pc"] self.st = kwargs["st"] self.jumpis = kwargs["jumpis"] - self.output = kwargs["output"] self.symbolic = kwargs["symbolic"] self.prank = kwargs["prank"] + self.addresses_to_delete = kwargs.get("addresses_to_delete") or set() # - self.solver = kwargs["solver"] self.path = kwargs["path"] + self.alias = kwargs["alias"] # - self.log = kwargs["log"] self.cnts = kwargs["cnts"] self.sha3s = kwargs["sha3s"] self.storages = kwargs["storages"] self.balances = kwargs["balances"] self.calls = kwargs["calls"] - self.failed = kwargs["failed"] - self.error = kwargs["error"] - assert_address(self.caller) + assert_address(self.context.message.target) + assert_address(self.context.message.caller) assert_address(self.this) + def context_str(self) -> str: + opcode = self.current_opcode() + return f"addr={hexify(self.this)} pc={self.pc} insn={mnemonic(opcode)}" + + def halt( + self, + data: Bytes = EMPTY_BYTES, + error: Optional[EvmException] = None, + ) -> None: + output = self.context.output + if output.data is not None: + raise HalmosException("output already set") + + output.data = data + output.error = error + output.return_scheme = self.current_opcode() + + def emit_log(self, log: EventLog): + self.context.trace.append(log) + + def calldata(self) -> List[Byte]: + message = self.message() + return [] if message.is_create() else message.data + + def caller(self): + return self.message().caller + + def callvalue(self): + return self.message().value + + def message(self): + return self.context.message + def current_opcode(self) -> UnionType[int, BitVecRef]: - return unbox_int(self.code[self.this][self.pc]) + return unbox_int(self.pgm[self.pc]) def current_instruction(self) -> Instruction: - return self.code[self.this].decode_instruction(self.pc) + return self.pgm.decode_instruction(self.pc) + + def set_code( + self, who: Address, code: UnionType[bytes, BitVecRef, Contract] + ) -> None: + """ + Sets the code at a given address. + """ + assert_address(who) + self.code[who] = code if isinstance(code, Contract) else Contract(code) def str_cnts(self) -> str: return "".join( @@ -673,18 +857,8 @@ def str_cnts(self) -> str: ] ) - def str_solver(self) -> str: - return "\n".join([str(cond) for cond in self.solver.assertions()]) - - def str_path(self) -> str: - return "".join( - map( - lambda x: "- " + str(x) + "\n", - filter(lambda x: str(x) != "True", self.path), - ) - ) - def __str__(self) -> str: + output = self.context.output.data return hexify( "".join( [ @@ -698,12 +872,10 @@ def __str__(self) -> str: self.storage, ) ), - # f"Solver:\n{self.str_solver()}\n", - f"Path:\n{self.str_path()}", - f"Output: {self.output.hex() if isinstance(self.output, bytes) else self.output}\n", - f"Log: {self.log}\n", - # f"Opcodes:\n{self.str_cnts()}", - # f"Memsize: {len(self.st.memory)}\n", + f"Path:\n{self.path}", + f"Aliases:\n", + "".join([f"- {k}: {v}\n" for k, v in self.alias.items()]), + f"Output: {output.hex() if isinstance(output, bytes) else output}\n", f"Balance updates:\n", "".join( map( @@ -719,23 +891,26 @@ def __str__(self) -> str: ) ), f"SHA3 hashes:\n", - "".join(map(lambda x: f"- {x}\n", self.sha3s)), + "".join(map(lambda x: f"- {self.sha3s[x]}: {x}\n", self.sha3s)), f"External calls:\n", "".join(map(lambda x: f"- {x}\n", self.calls)), - # f"Calldata: {self.calldata}\n", ] ) ) def next_pc(self) -> None: - self.pc = self.code[self.this].next_pc(self.pc) + self.pc = self.pgm.next_pc(self.pc) def check(self, cond: Any) -> Any: - self.solver.push() - self.solver.add(simplify(cond)) - result = self.solver.check() - self.solver.pop() - return result + cond = simplify(cond) + + if is_true(cond): + return sat + + if is_false(cond): + return unsat + + return self.path.solver.check(cond) def select(self, array: Any, key: Word, arrays: Dict) -> Word: if array in arrays: @@ -760,121 +935,251 @@ def balance_of(self, addr: Word) -> Word: assert_address(addr) value = self.select(self.balance, addr, self.balances) # practical assumption on the max balance per account - self.solver.add(ULT(value, con(2**96))) + self.path.append(ULT(value, con(2**96))) return value - def balance_update(self, addr: Word, value: Word): + def balance_update(self, addr: Word, value: Word) -> None: assert_address(addr) assert_uint256(value) new_balance_var = Array( - f"balance_{1+len(self.balances):>02}", BitVecSort(160), BitVecSort(256) + f"balance_{1+len(self.balances):>02}", BitVecSort160, BitVecSort256 ) new_balance = Store(self.balance, addr, value) - self.solver.add(new_balance_var == new_balance) + self.path.append(new_balance_var == new_balance) self.balance = new_balance_var self.balances[new_balance_var] = new_balance - def empty_storage_of(self, addr: BitVecRef, slot: int, len_keys: int) -> ArrayRef: + def sha3(self) -> None: + loc: int = self.st.mloc() + size: int = int_of(self.st.pop(), "symbolic SHA3 data size") + if size > 0: + sha3_image = self.sha3_data(wload(self.st.memory, loc, size), size) + else: + sha3_image = self.sha3_data(None, size) + self.st.push(sha3_image) + + def sha3_data(self, data: Bytes, size: int) -> Word: + if size > 0: + f_sha3 = Function(f"sha3_{size * 8}", BitVecSorts[size * 8], BitVecSort256) + sha3_expr = f_sha3(data) + else: + sha3_expr = BitVec("sha3_0", BitVecSort256) + + # assume hash values are sufficiently smaller than the uint max + self.path.append(ULE(sha3_expr, con(2**256 - 2**64))) + + # assume no hash collision + self.assume_sha3_distinct(sha3_expr) + + # handle create2 hash + if size == 85 and eq(extract_bytes(data, 0, 1), con(0xFF, 8)): + return con(create2_magic_address + self.sha3s[sha3_expr]) + else: + return sha3_expr + + def assume_sha3_distinct(self, sha3_expr) -> None: + # skip if already exist + if sha3_expr in self.sha3s: + return + + # we expect sha3_expr to be `sha3_(input_expr)` + sha3_decl_name = sha3_expr.decl().name() + + for prev_sha3_expr in self.sha3s: + if prev_sha3_expr.decl().name() == sha3_decl_name: + # inputs have the same size: assume different inputs + # lead to different outputs + self.path.append( + Implies( + sha3_expr.arg(0) != prev_sha3_expr.arg(0), + sha3_expr != prev_sha3_expr, + ) + ) + else: + # inputs have different sizes: assume the outputs are different + self.path.append(sha3_expr != prev_sha3_expr) + + self.path.append(sha3_expr != con(0)) + self.sha3s[sha3_expr] = len(self.sha3s) + + def new_gas_id(self) -> int: + self.cnts["fresh"]["gas"] += 1 + return self.cnts["fresh"]["gas"] + + def new_address(self) -> Address: + self.cnts["fresh"]["address"] += 1 + return con_addr( + magic_address + new_address_offset + self.cnts["fresh"]["address"] + ) + + def new_symbol_id(self) -> int: + self.cnts["fresh"]["symbol"] += 1 + return self.cnts["fresh"]["symbol"] + + def returndata(self) -> Optional[Bytes]: + """ + Return data from the last executed sub-context or the empty bytes sequence + """ + + last_subcall = self.context.last_subcall() + + if not last_subcall: + return EMPTY_BYTES + + output = last_subcall.output + if last_subcall.message.is_create() and not output.error: + return EMPTY_BYTES + + return output.data + + def returndatasize(self) -> int: + returndata = self.returndata() + return byte_length(returndata) if returndata is not None else 0 + + def is_jumpdest(self, x: Word) -> bool: + if not is_concrete(x): + return False + + pc: int = int_of(x) + if pc < 0: + raise ValueError(pc) + + return pc in self.pgm.valid_jump_destinations() + + def jumpi_id(self) -> str: + return f"{self.pc}:" + ",".join( + map(lambda x: str(x) if self.is_jumpdest(x) else "", self.st.stack) + ) + + # deploy libraries and resolve library placeholders in hexcode + def resolve_libs(self, creation_hexcode, deployed_hexcode, lib_references) -> str: + if lib_references: + for lib in lib_references: + address = self.new_address() + + self.code[address] = Contract.from_hexcode( + lib_references[lib]["hexcode"] + ) + + placeholder = lib_references[lib]["placeholder"] + hex_address = stripped(hex(address.as_long())).zfill(40) + + creation_hexcode = creation_hexcode.replace(placeholder, hex_address) + deployed_hexcode = deployed_hexcode.replace(placeholder, hex_address) + + return (creation_hexcode, deployed_hexcode) + + +class Storage: + pass + + +class SolidityStorage(Storage): + @classmethod + def empty(cls, addr: BitVecRef, slot: int, keys: Tuple) -> ArrayRef: + num_keys = len(keys) + size_keys = cls.bitsize(keys) return Array( - f"storage_{id_str(addr)}_{slot}_{len_keys}_00", - BitVecSort(len_keys * 256), - BitVecSort(256), + f"storage_{id_str(addr)}_{slot}_{num_keys}_{size_keys}_00", + BitVecSorts[size_keys], + BitVecSort256, ) - def sinit(self, addr: Any, slot: int, keys) -> None: + @classmethod + def init(cls, ex: Exec, addr: Any, slot: int, keys: Tuple) -> None: assert_address(addr) - if slot not in self.storage[addr]: - self.storage[addr][slot] = {} - if len(keys) not in self.storage[addr][slot]: - if len(keys) == 0: - if self.symbolic: - self.storage[addr][slot][len(keys)] = BitVec( - f"storage_{id_str(addr)}_{slot}_{len(keys)}_00", 256 + num_keys = len(keys) + size_keys = cls.bitsize(keys) + if slot not in ex.storage[addr]: + ex.storage[addr][slot] = {} + if num_keys not in ex.storage[addr][slot]: + ex.storage[addr][slot][num_keys] = {} + if size_keys not in ex.storage[addr][slot][num_keys]: + if size_keys == 0: + if ex.symbolic: + label = f"storage_{id_str(addr)}_{slot}_{num_keys}_{size_keys}_00" + ex.storage[addr][slot][num_keys][size_keys] = BitVec( + label, BitVecSort256 ) else: - self.storage[addr][slot][len(keys)] = con(0) + ex.storage[addr][slot][num_keys][size_keys] = con(0) else: - # do not use z3 const array `K(BitVecSort(len(keys)*256), con(0))` when not self.symbolic - # instead use normal smt array, and generate emptyness axiom; see sload() - self.storage[addr][slot][len(keys)] = self.empty_storage_of( - addr, slot, len(keys) + # do not use z3 const array `K(BitVecSort(size_keys), con(0))` when not ex.symbolic + # instead use normal smt array, and generate emptyness axiom; see load() + ex.storage[addr][slot][num_keys][size_keys] = cls.empty( + addr, slot, keys ) - def sload(self, addr: Any, loc: Word) -> Word: - offsets = self.decode_storage_loc(loc) + @classmethod + def load(cls, ex: Exec, addr: Any, loc: Word) -> Word: + offsets = cls.decode(loc) if not len(offsets) > 0: raise ValueError(offsets) slot, keys = int_of(offsets[0], "symbolic storage base slot"), offsets[1:] - self.sinit(addr, slot, keys) - if len(keys) == 0: - return self.storage[addr][slot][0] + cls.init(ex, addr, slot, keys) + num_keys = len(keys) + size_keys = cls.bitsize(keys) + if num_keys == 0: + return ex.storage[addr][slot][num_keys][size_keys] else: - if not self.symbolic: - # generate emptyness axiom for each array index, instead of using quantified formula; see sinit() - self.solver.add( - Select(self.empty_storage_of(addr, slot, len(keys)), concat(keys)) - == con(0) + if not ex.symbolic: + # generate emptyness axiom for each array index, instead of using quantified formula; see init() + ex.path.append( + Select(cls.empty(addr, slot, keys), concat(keys)) == con(0) ) - return self.select( - self.storage[addr][slot][len(keys)], concat(keys), self.storages + return ex.select( + ex.storage[addr][slot][num_keys][size_keys], concat(keys), ex.storages ) - def sstore(self, addr: Any, loc: Any, val: Any) -> None: - offsets = self.decode_storage_loc(loc) + @classmethod + def store(cls, ex: Exec, addr: Any, loc: Any, val: Any) -> None: + offsets = cls.decode(loc) if not len(offsets) > 0: raise ValueError(offsets) slot, keys = int_of(offsets[0], "symbolic storage base slot"), offsets[1:] - self.sinit(addr, slot, keys) - if len(keys) == 0: - self.storage[addr][slot][0] = val + cls.init(ex, addr, slot, keys) + num_keys = len(keys) + size_keys = cls.bitsize(keys) + if num_keys == 0: + ex.storage[addr][slot][num_keys][size_keys] = val else: new_storage_var = Array( - f"storage_{id_str(addr)}_{slot}_{len(keys)}_{1+len(self.storages):>02}", - BitVecSort(len(keys) * 256), - BitVecSort(256), + f"storage_{id_str(addr)}_{slot}_{num_keys}_{size_keys}_{1+len(ex.storages):>02}", + BitVecSorts[size_keys], + BitVecSort256, ) - new_storage = Store(self.storage[addr][slot][len(keys)], concat(keys), val) - self.solver.add(new_storage_var == new_storage) - self.storage[addr][slot][len(keys)] = new_storage_var - self.storages[new_storage_var] = new_storage - - def decode_storage_loc(self, loc: Any) -> Any: - def normalize(expr: Any) -> Any: - # Concat(Extract(255, 8, bvadd(x, y)), bvadd(Extract(7, 0, x), Extract(7, 0, y))) => x + y - if expr.decl().name() == "concat" and expr.num_args() == 2: - arg0 = expr.arg(0) # Extract(255, 8, bvadd(x, y)) - arg1 = expr.arg(1) # bvadd(Extract(7, 0, x), Extract(7, 0, y)) - if ( - arg0.decl().name() == "extract" - and arg0.num_args() == 1 - and arg0.params() == [255, 8] - ): - arg00 = arg0.arg(0) # bvadd(x, y) - if arg00.decl().name() == "bvadd": - x = arg00.arg(0) - y = arg00.arg(1) - if arg1.decl().name() == "bvadd" and arg1.num_args() == 2: - if eq(arg1.arg(0), simplify(Extract(7, 0, x))) and eq( - arg1.arg(1), simplify(Extract(7, 0, y)) - ): - return x + y - return expr + new_storage = Store( + ex.storage[addr][slot][num_keys][size_keys], concat(keys), val + ) + ex.path.append(new_storage_var == new_storage) + ex.storage[addr][slot][num_keys][size_keys] = new_storage_var + ex.storages[new_storage_var] = new_storage + @classmethod + def decode(cls, loc: Any) -> Any: loc = normalize(loc) - - if loc.decl().name() == "sha3_512": # m[k] : hash(k.m) + # m[k] : hash(k.m) + if loc.decl().name() == "sha3_512": args = loc.arg(0) - offset, base = simplify(Extract(511, 256, args)), simplify( - Extract(255, 0, args) - ) - return self.decode_storage_loc(base) + (offset, con(0)) - elif loc.decl().name() == "sha3_256": # a[i] : hash(a)+i + offset = simplify(Extract(511, 256, args)) + base = simplify(Extract(255, 0, args)) + return cls.decode(base) + (offset, con(0)) + # a[i] : hash(a) + i + elif loc.decl().name() == "sha3_256": base = loc.arg(0) - return self.decode_storage_loc(base) + (con(0),) + return cls.decode(base) + (con(0),) + # m[k] : hash(k.m) where |k| != 256-bit + elif loc.decl().name().startswith("sha3_"): + sha3_input = normalize(loc.arg(0)) + if sha3_input.decl().name() == "concat" and sha3_input.num_args() == 2: + offset = simplify(sha3_input.arg(0)) + base = simplify(sha3_input.arg(1)) + if offset.size() != 256 and base.size() == 256: + return cls.decode(base) + (offset, con(0)) elif loc.decl().name() == "bvadd": # # when len(args) == 2 - # arg0 = self.decode_storage_loc(loc.arg(0)) - # arg1 = self.decode_storage_loc(loc.arg(1)) + # arg0 = cls.decode(loc.arg(0)) + # arg1 = cls.decode(loc.arg(1)) # if len(arg0) == 1 and len(arg1) > 1: # i + hash(x) # return arg1[0:-1] + (arg1[-1] + arg0[0],) # elif len(arg0) > 1 and len(arg1) == 1: # hash(x) + i @@ -887,9 +1192,7 @@ def normalize(expr: Any) -> Any: args = loc.children() if len(args) < 2: raise ValueError(loc) - args = sorted( - map(self.decode_storage_loc, args), key=lambda x: len(x), reverse=True - ) + args = sorted(map(cls.decode, args), key=lambda x: len(x), reverse=True) if len(args[1]) > 1: # only args[0]'s length >= 1, the others must be 1 raise ValueError(loc) @@ -902,125 +1205,138 @@ def normalize(expr: Any) -> Any: return (con(preimage), con(delta)) else: return (loc,) - elif is_bv(loc): + + if is_bv(loc): return (loc,) else: raise ValueError(loc) - def sha3(self) -> None: - loc: int = self.st.mloc() - size: int = int_of(self.st.pop(), "symbolic SHA3 data size") - self.sha3_data(wload(self.st.memory, loc, size), size) + @classmethod + def bitsize(cls, keys: Tuple) -> int: + size = sum([key.size() for key in keys]) + if len(keys) > 0 and size == 0: + raise ValueError(keys) + return size - def sha3_data(self, data: Bytes, size: int) -> None: - f_sha3 = Function( - "sha3_" + str(size * 8), BitVecSort(size * 8), BitVecSort(256) - ) - sha3 = f_sha3(data) - sha3_var = BitVec(f"sha3_var_{len(self.sha3s):>02}", 256) - self.solver.add(sha3_var == sha3) - # assume hash values are sufficiently smaller than the uint max - self.solver.add(ULE(sha3_var, con(2**256 - 2**64))) - self.assume_sha3_distinct(sha3_var, sha3) - if size == 64 or size == 32: # for storage hashed location - self.st.push(sha3) - else: - self.st.push(sha3_var) - def assume_sha3_distinct(self, sha3_var, sha3) -> None: - for v, s in self.sha3s: - if s.decl().name() == sha3.decl().name(): # same size - # self.solver.add(Implies(sha3_var == v, sha3.arg(0) == s.arg(0))) - self.solver.add(Implies(sha3.arg(0) != s.arg(0), sha3_var != v)) - else: - self.solver.add(sha3_var != v) - self.solver.add(sha3_var != con(0)) - self.sha3s.append((sha3_var, sha3)) - - def new_gas_id(self) -> int: - self.cnts["fresh"]["gas"] += 1 - return self.cnts["fresh"]["gas"] - - def new_address(self) -> Address: - self.cnts["fresh"]["address"] += 1 - return con_addr( - magic_address + new_address_offset + self.cnts["fresh"]["address"] +class GenericStorage(Storage): + @classmethod + def empty(cls, addr: BitVecRef, loc: BitVecRef) -> ArrayRef: + return Array( + f"storage_{id_str(addr)}_{loc.size()}_00", + BitVecSorts[loc.size()], + BitVecSort256, ) - def new_symbol_id(self) -> int: - self.cnts["fresh"]["symbol"] += 1 - return self.cnts["fresh"]["symbol"] - - def returndatasize(self) -> int: - return 0 if self.output is None else byte_length(self.output) - - def is_jumpdest(self, x: Word) -> bool: - if not is_concrete(x): - return False - - pc: int = int_of(x) - if pc < 0: - raise ValueError(pc) - - opcode = unbox_int(self.code[self.this][pc]) - return opcode == EVM.JUMPDEST - - def jumpi_id(self) -> str: - return f"{self.pc}:" + ",".join( - map(lambda x: str(x) if self.is_jumpdest(x) else "", self.st.stack) + @classmethod + def init(cls, ex: Exec, addr: Any, loc: BitVecRef) -> None: + assert_address(addr) + if loc.size() not in ex.storage[addr]: + ex.storage[addr][loc.size()] = cls.empty(addr, loc) + + @classmethod + def load(cls, ex: Exec, addr: Any, loc: Word) -> Word: + loc = cls.decode(loc) + cls.init(ex, addr, loc) + if not ex.symbolic: + # generate emptyness axiom for each array index, instead of using quantified formula; see init() + ex.path.append(Select(cls.empty(addr, loc), loc) == con(0)) + return ex.select(ex.storage[addr][loc.size()], loc, ex.storages) + + @classmethod + def store(cls, ex: Exec, addr: Any, loc: Any, val: Any) -> None: + loc = cls.decode(loc) + cls.init(ex, addr, loc) + new_storage_var = Array( + f"storage_{id_str(addr)}_{loc.size()}_{1+len(ex.storages):>02}", + BitVecSorts[loc.size()], + BitVecSort256, ) + new_storage = Store(ex.storage[addr][loc.size()], loc, val) + ex.path.append(new_storage_var == new_storage) + ex.storage[addr][loc.size()] = new_storage_var + ex.storages[new_storage_var] = new_storage + @classmethod + def decode(cls, loc: Any) -> Any: + loc = normalize(loc) + if loc.decl().name() == "sha3_512": # hash(hi,lo), recursively + args = loc.arg(0) + hi = cls.decode(simplify(Extract(511, 256, args))) + lo = cls.decode(simplify(Extract(255, 0, args))) + return cls.simple_hash(Concat(hi, lo)) + elif loc.decl().name().startswith("sha3_"): + sha3_input = normalize(loc.arg(0)) + if sha3_input.decl().name() == "concat": + decoded_sha3_input_args = [ + cls.decode(sha3_input.arg(i)) for i in range(sha3_input.num_args()) + ] + return cls.simple_hash(concat(decoded_sha3_input_args)) + else: + return cls.simple_hash(cls.decode(sha3_input)) + elif loc.decl().name() == "bvadd": + args = loc.children() + if len(args) < 2: + raise ValueError(loc) + return cls.add_all([cls.decode(arg) for arg in args]) + elif is_bv_value(loc): + (preimage, delta) = restore_precomputed_hashes(loc.as_long()) + if preimage: # loc == hash(preimage) + delta + return cls.add_all([cls.simple_hash(con(preimage)), con(delta)]) + else: + return loc -# x == b if sort(x) = bool -# int_to_bool(x) == b if sort(x) = int -def test(x: Word, b: bool) -> Word: - if is_bool(x): - if b: - return x - else: - return Not(x) - elif is_bv(x): - if b: - return x != con(0) + if is_bv(loc): + return loc else: - return x == con(0) - else: - raise ValueError(x) + raise ValueError(loc) + @classmethod + def simple_hash(cls, x: BitVecRef) -> BitVecRef: + # simple injective function for collision-free (though not secure) hash semantics, comprising: + # - left-shift by 256 bits to ensure sufficient logical domain space + # - an additional 1-bit for disambiguation (e.g., between map[key] vs array[i][j]) + return simplify(Concat(x, con(0, 257))) -def is_non_zero(x: Word) -> Word: - return test(x, True) + @classmethod + def add_all(cls, args: List) -> BitVecRef: + bitsize = max([x.size() for x in args]) + res = con(0, bitsize) + for x in args: + if x.size() < bitsize: + x = simplify(ZeroExt(bitsize - x.size(), x)) + res += x + return simplify(res) -def is_zero(x: Word) -> Word: - return test(x, False) +SomeStorage = TypeVar("SomeStorage", bound=Storage) -def and_or(x: Word, y: Word, is_and: bool) -> Word: +def bitwise(op, x: Word, y: Word) -> Word: if is_bool(x) and is_bool(y): - if is_and: + if op == EVM.AND: return And(x, y) - else: + elif op == EVM.OR: return Or(x, y) + elif op == EVM.XOR: + return Xor(x, y) + else: + raise ValueError(op, x, y) elif is_bv(x) and is_bv(y): - if is_and: + if op == EVM.AND: return x & y - else: + elif op == EVM.OR: return x | y + elif op == EVM.XOR: + return x ^ y # bvxor + else: + raise ValueError(op, x, y) elif is_bool(x) and is_bv(y): - return and_or(If(x, con(1), con(0)), y, is_and) + return bitwise(op, If(x, con(1), con(0)), y) elif is_bv(x) and is_bool(y): - return and_or(x, If(y, con(1), con(0)), is_and) + return bitwise(op, x, If(y, con(1), con(0))) else: - raise ValueError(x, y, is_and) - - -def and_of(x: Word, y: Word) -> Word: - return and_or(x, y, True) - - -def or_of(x: Word, y: Word) -> Word: - return and_or(x, y, False) + raise ValueError(op, x, y) def b2i(w: Word) -> Word: @@ -1041,11 +1357,47 @@ def is_power_of_two(x: int) -> bool: return False +class HalmosLogs: + bounded_loops: List[str] + unknown_calls: Dict[str, Dict[str, Set[str]]] # funsig -> to -> set(arg) + + def __init__(self) -> None: + self.bounded_loops = [] + self.unknown_calls = defaultdict(lambda: defaultdict(set)) + + def extend(self, logs: "HalmosLogs") -> None: + self.bounded_loops.extend(logs.bounded_loops) + for funsig in logs.unknown_calls: + for to in logs.unknown_calls[funsig]: + self.unknown_calls[funsig][to].update(logs.unknown_calls[funsig][to]) + + def add_uninterpreted_unknown_call(self, funsig, to, arg): + funsig, to, arg = hexify(funsig), hexify(to), hexify(arg) + self.unknown_calls[funsig][to].add(arg) + + def print_unknown_calls(self): + for funsig in self.unknown_calls: + print(f"{funsig}:") + for to in self.unknown_calls[funsig]: + print(f"- {to}:") + print( + "\n".join([f" - {arg}" for arg in self.unknown_calls[funsig][to]]) + ) + + class SEVM: options: Dict + storage_model: Type[SomeStorage] + logs: HalmosLogs + steps: Steps def __init__(self, options: Dict) -> None: self.options = options + self.logs = HalmosLogs() + self.steps: Steps = {} + + is_generic = self.options["storage_layout"] == "generic" + self.storage_model = GenericStorage if is_generic else SolidityStorage def div_xy_y(self, w1: Word, w2: Word) -> Word: # return the number of bits required to represent the given value. default = 256 @@ -1058,6 +1410,8 @@ def bitsize(w: Word) -> int: return 256 - w.arg(0).size() return 256 + w1 = normalize(w1) + if w1.decl().name() == "bvmul" and w1.num_args() == 2: x = w1.arg(0) y = w1.arg(1) @@ -1071,135 +1425,126 @@ def bitsize(w: Word) -> int: return x return None - def mk_add(self, x: Any, y: Any) -> Any: - f_add[x.size()](x, y) - - def mk_mul(self, x: Any, y: Any) -> Any: - f_mul[x.size()](x, y) - def mk_div(self, ex: Exec, x: Any, y: Any) -> Any: term = f_div(x, y) - ex.solver.add(ULE(term, x)) # (x / y) <= x + ex.path.append(ULE(term, x)) # (x / y) <= x return term def mk_mod(self, ex: Exec, x: Any, y: Any) -> Any: term = f_mod[x.size()](x, y) - ex.solver.add(ULE(term, y)) # (x % y) <= y - # ex.solver.add(Or(y == con(0), ULT(term, y))) # (x % y) < y if y != 0 + ex.path.append(ULE(term, y)) # (x % y) <= y + # ex.path.append(Or(y == con(0), ULT(term, y))) # (x % y) < y if y != 0 return term def arith(self, ex: Exec, op: int, w1: Word, w2: Word) -> Word: w1 = b2i(w1) w2 = b2i(w2) + if op == EVM.ADD: - if self.options.get("add"): - return w1 + w2 - if is_bv_value(w1) and is_bv_value(w2): - return w1 + w2 - else: - return mk_add(w1, w2) - elif op == EVM.SUB: - if self.options.get("sub"): - return w1 - w2 - if is_bv_value(w1) and is_bv_value(w2): - return w1 - w2 - else: - return f_sub(w1, w2) - elif op == EVM.MUL: - if self.options.get("mul"): - return w1 * w2 - if is_bv_value(w1) and is_bv_value(w2): - return w1 * w2 - elif is_bv_value(w1): - i1: int = int(str(w1)) # must be concrete - if i1 == 0: - return w1 - elif is_power_of_two(i1): - return w2 << int(math.log(i1, 2)) - else: - return mk_mul(w1, w2) - elif is_bv_value(w2): - i2: int = int(str(w2)) # must be concrete - if i2 == 0: - return w2 - elif is_power_of_two(i2): - return w1 << int(math.log(i2, 2)) - else: - return mk_mul(w1, w2) - else: - return mk_mul(w1, w2) - elif op == EVM.DIV: + return w1 + w2 + + if op == EVM.SUB: + return w1 - w2 + + if op == EVM.MUL: + return w1 * w2 + + if op == EVM.DIV: div_for_overflow_check = self.div_xy_y(w1, w2) if div_for_overflow_check is not None: # xy/x or xy/y return div_for_overflow_check - if self.options.get("div"): - return UDiv(w1, w2) # unsigned div (bvudiv) + if is_bv_value(w1) and is_bv_value(w2): - return UDiv(w1, w2) - elif is_bv_value(w2): - i2: int = int(str(w2)) # must be concrete + if w2.as_long() == 0: + return w2 + else: + return UDiv(w1, w2) # unsigned div (bvudiv) + + if is_bv_value(w2): + # concrete denominator case + i2: int = w2.as_long() if i2 == 0: return w2 - elif i2 == 1: + + if i2 == 1: return w1 - elif is_power_of_two(i2): + + if is_power_of_two(i2): return LShR(w1, int(math.log(i2, 2))) - elif self.options.get("divByConst"): - return UDiv(w1, w2) - else: - return self.mk_div(ex, w1, w2) - else: - return self.mk_div(ex, w1, w2) - elif op == EVM.MOD: - if self.options.get("mod"): - return URem(w1, w2) + + return self.mk_div(ex, w1, w2) + + if op == EVM.MOD: if is_bv_value(w1) and is_bv_value(w2): - return URem(w1, w2) # bvurem - elif is_bv_value(w2): + if w2.as_long() == 0: + return w2 + else: + return URem(w1, w2) # bvurem + + if is_bv_value(w2): i2: int = int(str(w2)) if i2 == 0 or i2 == 1: return con(0, w2.size()) - elif is_power_of_two(i2): + + if is_power_of_two(i2): bitsize = int(math.log(i2, 2)) return ZeroExt(w2.size() - bitsize, Extract(bitsize - 1, 0, w1)) - elif self.options.get("modByConst"): - return URem(w1, w2) - else: - return self.mk_mod(ex, w1, w2) - else: - return self.mk_mod(ex, w1, w2) - elif op == EVM.SDIV: + + return self.mk_mod(ex, w1, w2) + + if op == EVM.SDIV: if is_bv_value(w1) and is_bv_value(w2): - return w1 / w2 # bvsdiv - else: - return f_sdiv(w1, w2) - elif op == EVM.SMOD: + if w2.as_long() == 0: + return w2 + else: + return w1 / w2 # bvsdiv + + if is_bv_value(w2): + # concrete denominator case + i2: int = w2.as_long() + if i2 == 0: + return w2 # div by 0 is 0 + + if i2 == 1: + return w1 # div by 1 is identity + + # fall back to uninterpreted function :( + return f_sdiv(w1, w2) + + if op == EVM.SMOD: if is_bv_value(w1) and is_bv_value(w2): - return SRem(w1, w2) # bvsrem # vs: w1 % w2 (bvsmod w1 w2) - else: - return f_smod(w1, w2) - elif op == EVM.EXP: + if w2.as_long() == 0: + return w2 + else: + return SRem(w1, w2) # bvsrem # vs: w1 % w2 (bvsmod w1 w2) + + # TODO: if is_bv_value(w2): + + return f_smod(w1, w2) + + if op == EVM.EXP: if is_bv_value(w1) and is_bv_value(w2): i1: int = int(str(w1)) # must be concrete i2: int = int(str(w2)) # must be concrete return con(i1**i2) - elif is_bv_value(w2): + + if is_bv_value(w2): i2: int = int(str(w2)) if i2 == 0: return con(1) - elif i2 == 1: + + if i2 == 1: return w1 - elif i2 <= self.options.get("expByConst"): + + if i2 <= self.options.get("expByConst"): exp = w1 for _ in range(i2 - 1): exp = exp * w1 return exp - else: - return f_exp(w1, w2) - else: - return f_exp(w1, w2) - else: - raise ValueError(op) + + return f_exp(w1, w2) + + raise ValueError(op) def arith2(self, ex: Exec, op: int, w1: Word, w2: Word, w3: Word) -> Word: w1 = b2i(w1) @@ -1230,28 +1575,80 @@ def arith2(self, ex: Exec, op: int, w1: Word, w2: Word, w3: Word) -> Word: else: raise ValueError(op) + def sload(self, ex: Exec, addr: Any, loc: Word) -> Word: + return self.storage_model.load(ex, addr, loc) + + def sstore(self, ex: Exec, addr: Any, loc: Any, val: Any) -> None: + if ex.message().is_static: + raise WriteInStaticContext(ex.context_str()) + + if is_bool(val): + val = If(val, con(1), con(0)) + + self.storage_model.store(ex, addr, loc, val) + + def resolve_address_alias(self, ex: Exec, target: Address) -> Address: + if target in ex.code: + return target + + # set new timeout temporarily for this task + ex.path.solver.set(timeout=max(1000, self.options["timeout"])) + + if target not in ex.alias: + for addr in ex.code: + if ex.check(target != addr) == unsat: # target == addr + if self.options.get("debug"): + print( + f"[DEBUG] Address alias: {hexify(addr)} for {hexify(target)}" + ) + ex.alias[target] = addr + ex.path.append(target == addr) + break + + # reset timeout + ex.path.solver.set(timeout=self.options["timeout"]) + + return ex.alias.get(target) + + def transfer_value( + self, + ex: Exec, + caller: Address, + to: Address, + value: Word, + condition: Word = None, + ) -> None: + # no-op if value is zero + if is_bv_value(value) and value.as_long() == 0: + return + + # assume balance is enough; otherwise ignore this path + # note: evm requires enough balance even for self-transfer + balance_cond = simplify(UGE(ex.balance_of(caller), value)) + ex.path.append(balance_cond) + + # conditional transfer + if condition is not None: + value = If(condition, value, con(0)) + + ex.balance_update(caller, self.arith(ex, EVM.SUB, ex.balance_of(caller), value)) + ex.balance_update(to, self.arith(ex, EVM.ADD, ex.balance_of(to), value)) + def call( self, ex: Exec, op: int, stack: List[Tuple[Exec, int]], step_id: int, - out: List[Exec], - bounded_loops: List[str], ) -> None: gas = ex.st.pop() - to = uint160(ex.st.pop()) + fund = con(0) if op in [EVM.STATICCALL, EVM.DELEGATECALL] else ex.st.pop() - if op == EVM.STATICCALL: - fund = con(0) - else: - fund = ex.st.pop() arg_loc: int = ex.st.mloc() - # size (in bytes) arg_size: int = int_of(ex.st.pop(), "symbolic CALL input data size") + ret_loc: int = ex.st.mloc() - # size (in bytes) ret_size: int = int_of(ex.st.pop(), "symbolic CALL return data size") if not arg_size >= 0: @@ -1259,75 +1656,67 @@ def call( if not ret_size >= 0: raise ValueError(ret_size) + arg = wload(ex.st.memory, arg_loc, arg_size) if arg_size > 0 else None caller = ex.prank.lookup(ex.this, to) - if not (is_bv_value(fund) and fund.as_long() == 0): - ex.balance_update( - caller, self.arith(ex, EVM.SUB, ex.balance_of(caller), fund) - ) - ex.balance_update(to, self.arith(ex, EVM.ADD, ex.balance_of(to), fund)) + def send_callvalue(condition=None) -> None: + # no balance update for CALLCODE which transfers to itself + if op == EVM.CALL: + # TODO: revert if context is static + self.transfer_value(ex, caller, to, fund, condition) + + def call_known(to: Address) -> None: + # backup current state + orig_code = ex.code.copy() + orig_storage = deepcopy(ex.storage) + orig_balance = ex.balance - def call_known() -> None: + # transfer msg.value + send_callvalue() + + # prepare calldata calldata = [None] * arg_size wextend(ex.st.memory, arg_loc, arg_size) wstore_bytes( calldata, 0, arg_size, ex.st.memory[arg_loc : arg_loc + arg_size] ) - # execute external calls - (new_exs, new_steps, new_bounded_loops) = self.run( - Exec( - code=ex.code, - storage=ex.storage, - balance=ex.balance, - # - block=ex.block, - # - calldata=calldata, - callvalue=fund, - caller=caller, - this=to, - # - pc=0, - st=State(), - jumpis={}, - output=None, - symbolic=ex.symbolic, - prank=Prank(), - # - solver=ex.solver, - path=ex.path, - # - log=ex.log, - cnts=ex.cnts, - sha3s=ex.sha3s, - storages=ex.storages, - balances=ex.balances, - calls=ex.calls, - failed=ex.failed, - error=ex.error, - ) + message = Message( + target=to if op in [EVM.CALL, EVM.STATICCALL] else ex.this, + caller=caller if op != EVM.DELEGATECALL else ex.caller(), + value=fund if op != EVM.DELEGATECALL else ex.callvalue(), + data=calldata, + is_static=(ex.context.message.is_static or op == EVM.STATICCALL), + call_scheme=op, ) - bounded_loops.extend(new_bounded_loops) + # TODO: check max call depth - # process result - for idx, new_ex in enumerate(new_exs): - opcode = new_ex.current_opcode() + def callback(new_ex, stack, step_id): + # continue execution in the context of the parent + # pessimistic copy because the subcall results may diverge + subcall = new_ex.context - # restore tx msg - new_ex.calldata = ex.calldata - new_ex.callvalue = ex.callvalue - new_ex.caller = ex.caller + # restore context + new_ex.context = deepcopy(ex.context) + new_ex.context.trace.append(subcall) new_ex.this = ex.this + new_ex.callback = ex.callback + + if subcall.is_stuck(): + # internal errors abort the current path, + # so we don't need to add it to the worklist + yield new_ex + return + # restore vm state + new_ex.pgm = ex.pgm new_ex.pc = ex.pc new_ex.st = deepcopy(ex.st) new_ex.jumpis = deepcopy(ex.jumpis) - # new_ex.output is passed into the caller new_ex.symbolic = ex.symbolic - new_ex.prank = ex.prank + new_ex.prank = deepcopy(ex.prank) # set return data (in memory) actual_ret_size = new_ex.returndatasize() @@ -1336,489 +1725,348 @@ def call_known() -> None: ret_loc, 0, min(ret_size, actual_ret_size), - new_ex.output, + subcall.output.data, actual_ret_size, ) - # set status code (in stack) - if opcode in [EVM.STOP, EVM.RETURN, EVM.REVERT, EVM.INVALID]: - if opcode in [EVM.STOP, EVM.RETURN]: - new_ex.st.push(con(1)) - else: - new_ex.st.push(con(0)) + # set status code on the stack + subcall_success = subcall.output.error is None + new_ex.st.push(con(1) if subcall_success else con(0)) - # add to worklist even if it reverted during the external call - new_ex.next_pc() - stack.append((new_ex, step_id)) - else: - # got stuck during external call - new_ex.error = f"External call encountered an issue at {mnemonic(opcode)}: {new_ex.error}" - out.append(new_ex) + if not subcall_success: + # revert network states + new_ex.code = orig_code.copy() + new_ex.storage = deepcopy(orig_storage) + new_ex.balance = orig_balance + + # add to worklist even if it reverted during the external call + new_ex.next_pc() + stack.append((new_ex, step_id)) + + sub_ex = Exec( + code=ex.code, + storage=ex.storage, + balance=ex.balance, + # + block=ex.block, + # + context=CallContext(message=message, depth=ex.context.depth + 1), + callback=callback, + this=message.target, + # + pgm=ex.code[to], + pc=0, + st=State(), + jumpis={}, + symbolic=ex.symbolic, + prank=Prank(), + # + path=ex.path, + alias=ex.alias, + # + cnts=ex.cnts, + sha3s=ex.sha3s, + storages=ex.storages, + balances=ex.balances, + calls=ex.calls, + ) + + stack.append((sub_ex, step_id)) def call_unknown() -> None: call_id = len(ex.calls) # push exit code if arg_size > 0: - arg = wload(ex.st.memory, arg_loc, arg_size) f_call = Function( "call_" + str(arg_size * 8), - BitVecSort(256), # cnt - BitVecSort(256), # gas - BitVecSort(160), # to - BitVecSort(256), # value - BitVecSort(arg_size * 8), # args - BitVecSort(256), + BitVecSort256, # cnt + BitVecSort256, # gas + BitVecSort160, # to + BitVecSort256, # value + BitVecSorts[arg_size * 8], # args + BitVecSort256, ) exit_code = f_call(con(call_id), gas, to, fund, arg) else: f_call = Function( "call_" + str(arg_size * 8), - BitVecSort(256), # cnt - BitVecSort(256), # gas - BitVecSort(160), # to - BitVecSort(256), # value - BitVecSort(256), + BitVecSort256, # cnt + BitVecSort256, # gas + BitVecSort160, # to + BitVecSort256, # value + BitVecSort256, ) exit_code = f_call(con(call_id), gas, to, fund) - exit_code_var = BitVec(f"call_exit_code_{call_id:>02}", 256) - ex.solver.add(exit_code_var == exit_code) + exit_code_var = BitVec(f"call_exit_code_{call_id:>02}", BitVecSort256) + ex.path.append(exit_code_var == exit_code) ex.st.push(exit_code_var) - ret = None - # TODO: handle inconsistent return sizes for unknown functions + # transfer msg.value + send_callvalue(exit_code_var != con(0)) + if ret_size > 0: + # actual return data will be capped or zero-padded by ret_size + # FIX: this doesn't capture the case of returndatasize != ret_size + actual_ret_size = ret_size + else: + actual_ret_size = self.options["unknown_calls_return_size"] + + if actual_ret_size > 0: f_ret = Function( - "ret_" + str(ret_size * 8), - BitVecSort(256), - BitVecSort(ret_size * 8), + "ret_" + str(actual_ret_size * 8), + BitVecSort256, + BitVecSorts[actual_ret_size * 8], ) ret = f_ret(exit_code_var) + else: + ret = None # TODO: cover other precompiled - if eq(to, con_addr(1)): # ecrecover exit code is always 1 - ex.solver.add(exit_code_var != con(0)) - - # halmos cheat code - if eq(to, halmos_cheat_code.address): - ex.solver.add(exit_code_var != con(0)) - - funsig: int = int_of( - extract_funsig(arg), "symbolic halmos cheatcode function selector" - ) - - # createUint(uint256,string) returns (uint256) - if funsig == halmos_cheat_code.create_uint: - bit_size = int_of( - extract_bytes(arg, 4, 32), - "symbolic bit size for halmos.createUint()", - ) - label = name_of(extract_string_argument(arg, 1)) - if bit_size <= 256: - ret = uint256( - BitVec( - f"halmos_{label}_uint{bit_size}_{ex.new_symbol_id():>02}", - bit_size, - ) - ) - else: - ex.error = f"bitsize larger than 256: {bit_size}" - out.append(ex) - return - - # createBytes(uint256,string) returns (bytes) - elif funsig == halmos_cheat_code.create_bytes: - byte_size = int_of( - extract_bytes(arg, 4, 32), - "symbolic byte size for halmos.createBytes()", - ) - label = name_of(extract_string_argument(arg, 1)) - symbolic_bytes = BitVec( - f"halmos_{label}_bytes_{ex.new_symbol_id():>02}", byte_size * 8 - ) - ret = Concat( - BitVecVal(32, 256), BitVecVal(byte_size, 256), symbolic_bytes - ) - - # createUint256(string) returns (uint256) - elif funsig == halmos_cheat_code.create_uint256: - label = name_of(extract_string_argument(arg, 0)) - ret = BitVec( - f"halmos_{label}_uint256_{ex.new_symbol_id():>02}", 256 - ) - # createBytes32(string) returns (bytes32) - elif funsig == halmos_cheat_code.create_bytes32: - label = name_of(extract_string_argument(arg, 0)) - ret = BitVec( - f"halmos_{label}_bytes32_{ex.new_symbol_id():>02}", 256 - ) - - # createAddress(string) returns (address) - elif funsig == halmos_cheat_code.create_address: - label = name_of(extract_string_argument(arg, 0)) - ret = uint256( - BitVec(f"halmos_{label}_address_{ex.new_symbol_id():>02}", 160) - ) + # ecrecover + if eq(to, con_addr(1)): + ex.path.append(exit_code_var != con(0)) - # createBool(string) returns (bool) - elif funsig == halmos_cheat_code.create_bool: - label = name_of(extract_string_argument(arg, 0)) - ret = uint256( - BitVec(f"halmos_{label}_bool_{ex.new_symbol_id():>02}", 1) - ) + # identity + if eq(to, con_addr(4)): + ex.path.append(exit_code_var != con(0)) + ret = arg - else: - ex.error = f"Unknown halmos cheat code: function selector = 0x{funsig:0>8x}, calldata = {hexify(arg)}" - out.append(ex) - return + # halmos cheat code + if eq(to, halmos_cheat_code.address): + ex.path.append(exit_code_var != con(0)) + ret = halmos_cheat_code.handle(ex, arg) # vm cheat code if eq(to, hevm_cheat_code.address): - ex.solver.add(exit_code_var != con(0)) - # vm.fail() - # BitVecVal(hevm_cheat_code.fail_payload, 800) - if arg == hevm_cheat_code.fail_payload: - ex.failed = True - out.append(ex) - return - # vm.assume(bool) - elif ( - eq(arg.sort(), BitVecSort((4 + 32) * 8)) - and simplify(Extract(287, 256, arg)) == hevm_cheat_code.assume_sig - ): - assume_cond = simplify(is_non_zero(Extract(255, 0, arg))) - ex.solver.add(assume_cond) - ex.path.append(str(assume_cond)) - # vm.getCode(string) - elif ( - simplify(Extract(arg_size * 8 - 1, arg_size * 8 - 32, arg)) - == hevm_cheat_code.get_code_sig - ): - calldata = bytes.fromhex(hex(arg.as_long())[2:]) - path_len = int.from_bytes(calldata[36:68], "big") - path = calldata[68 : 68 + path_len].decode("utf-8") - - if ":" in path: - [filename, contract_name] = path.split(":") - path = "out/" + filename + "/" + contract_name + ".json" - - target = self.options["target"].rstrip("/") - path = target + "/" + path - - with open(path) as f: - artifact = json.loads(f.read()) - - if artifact["bytecode"]["object"]: - bytecode = artifact["bytecode"]["object"].replace("0x", "") - else: - bytecode = artifact["bytecode"].replace("0x", "") - - bytecode_len = (len(bytecode) + 1) // 2 - bytecode_len_enc = ( - hex(bytecode_len).replace("0x", "").rjust(64, "0") - ) - - bytecode_len_ceil = (bytecode_len + 31) // 32 * 32 - - ret_bytes = ( - "00" * 31 - + "20" - + bytecode_len_enc - + bytecode.ljust(bytecode_len_ceil * 2, "0") - ) - ret_len = len(ret_bytes) // 2 - ret_bytes = bytes.fromhex(ret_bytes) - - ret = BitVecVal(int.from_bytes(ret_bytes, "big"), ret_len * 8) - # vm.prank(address) - elif ( - eq(arg.sort(), BitVecSort((4 + 32) * 8)) - and simplify(Extract(287, 256, arg)) == hevm_cheat_code.prank_sig - ): - result = ex.prank.prank(uint160(Extract(255, 0, arg))) - if not result: - ex.error = "You have an active prank already." - out.append(ex) - return - # vm.startPrank(address) - elif ( - eq(arg.sort(), BitVecSort((4 + 32) * 8)) - and simplify(Extract(287, 256, arg)) - == hevm_cheat_code.start_prank_sig - ): - result = ex.prank.startPrank(uint160(Extract(255, 0, arg))) - if not result: - ex.error = "You have an active prank already." - out.append(ex) - return - # vm.stopPrank() - elif ( - eq(arg.sort(), BitVecSort((4) * 8)) - and simplify(Extract(31, 0, arg)) == hevm_cheat_code.stop_prank_sig - ): - ex.prank.stopPrank() - # vm.deal(address,uint256) - elif ( - eq(arg.sort(), BitVecSort((4 + 32 * 2) * 8)) - and simplify(Extract(543, 512, arg)) == hevm_cheat_code.deal_sig - ): - who = uint160(Extract(511, 256, arg)) - amount = simplify(Extract(255, 0, arg)) - ex.balance_update(who, amount) - # vm.store(address,bytes32,bytes32) - elif ( - eq(arg.sort(), BitVecSort((4 + 32 * 3) * 8)) - and simplify(Extract(799, 768, arg)) == hevm_cheat_code.store_sig - ): - store_account = uint160(Extract(767, 512, arg)) - store_slot = simplify(Extract(511, 256, arg)) - store_value = simplify(Extract(255, 0, arg)) - if store_account in ex.storage: - ex.sstore(store_account, store_slot, store_value) - else: - ex.error = f"uninitialized account: {store_account}" - out.append(ex) - return - # vm.load(address,bytes32) - elif ( - eq(arg.sort(), BitVecSort((4 + 32 * 2) * 8)) - and simplify(Extract(543, 512, arg)) == hevm_cheat_code.load_sig - ): - load_account = uint160(Extract(511, 256, arg)) - load_slot = simplify(Extract(255, 0, arg)) - if load_account in ex.storage: - ret = ex.sload(load_account, load_slot) - else: - ex.error = f"uninitialized account: {load_account}" - out.append(ex) - return - # vm.fee(uint256) - elif ( - eq(arg.sort(), BitVecSort((4 + 32) * 8)) - and simplify(Extract(287, 256, arg)) == hevm_cheat_code.fee_sig - ): - ex.block.basefee = simplify(Extract(255, 0, arg)) - # vm.chainId(uint256) - elif ( - eq(arg.sort(), BitVecSort((4 + 32) * 8)) - and simplify(Extract(287, 256, arg)) == hevm_cheat_code.chainid_sig - ): - ex.block.chainid = simplify(Extract(255, 0, arg)) - # vm.coinbase(address) - elif ( - eq(arg.sort(), BitVecSort((4 + 32) * 8)) - and simplify(Extract(287, 256, arg)) == hevm_cheat_code.coinbase_sig - ): - ex.block.coinbase = uint160(Extract(255, 0, arg)) - # vm.difficulty(uint256) - elif ( - eq(arg.sort(), BitVecSort((4 + 32) * 8)) - and simplify(Extract(287, 256, arg)) - == hevm_cheat_code.difficulty_sig - ): - ex.block.difficulty = simplify(Extract(255, 0, arg)) - # vm.roll(uint256) - elif ( - eq(arg.sort(), BitVecSort((4 + 32) * 8)) - and simplify(Extract(287, 256, arg)) == hevm_cheat_code.roll_sig - ): - ex.block.number = simplify(Extract(255, 0, arg)) - # vm.warp(uint256) - elif ( - eq(arg.sort(), BitVecSort((4 + 32) * 8)) - and simplify(Extract(287, 256, arg)) == hevm_cheat_code.warp_sig - ): - ex.block.timestamp = simplify(Extract(255, 0, arg)) - # vm.etch(address,bytes) - elif extract_funsig(arg) == hevm_cheat_code.etch_sig: - who = extract_bytes(arg, 4 + 12, 20) - - # who must be concrete - if not is_bv_value(who): - ex.error = f"vm.etch(address who, bytes code) must have concrete argument `who` but received {who}" - out.append(ex) - return - - # code must be concrete - try: - code_offset = int_of(extract_bytes(arg, 4 + 32, 32)) - code_length = int_of(extract_bytes(arg, 4 + code_offset, 32)) - code_int = int_of( - extract_bytes(arg, 4 + code_offset + 32, code_length) - ) - code_bytes = code_int.to_bytes(code_length, "big") - - ex.code[who] = Contract(code_bytes) - except Exception as e: - ex.error = f"vm.etch(address who, bytes code) must have concrete argument `code` but received calldata {arg}" - out.append(ex) - return - - else: - # TODO: support other cheat codes - ex.error = f"Unsupported cheat code: calldata = {hexify(arg)}" - out.append(ex) - return + ex.path.append(exit_code_var != con(0)) + ret = hevm_cheat_code.handle(self, ex, arg) # console if eq(to, console.address): - ex.solver.add(exit_code_var != con(0)) - - funsig: int = int_of( - extract_funsig(arg), "symbolic console function selector" - ) - - if funsig == console.log_uint: - print(extract_bytes(arg, 4, 32)) - - # elif funsig == console.log_string: - - else: - # TODO: support other console functions - ex.error = f"Unsupported console function: function selector = 0x{funsig:0>8x}, calldata = {hexify(arg)}" - out.append(ex) - return + ex.path.append(exit_code_var != con(0)) + console.handle(ex, arg) # store return value if ret_size > 0: wstore(ex.st.memory, ret_loc, ret_size, ret) - # propagate callee's output to caller, which could be None - ex.output = ret + ex.context.trace.append( + CallContext( + message=Message( + target=to, + caller=caller, + value=fund, + data=ex.st.memory[arg_loc : arg_loc + arg_size], + call_scheme=op, + ), + output=CallOutput( + data=ret, + error=None, + ), + depth=ex.context.depth + 1, + ) + ) - ex.calls.append((exit_code_var, exit_code, ex.output)) + # TODO: check if still needed + ex.calls.append((exit_code_var, exit_code, ex.context.output.data)) ex.next_pc() stack.append((ex, step_id)) - # separately handle known / unknown external calls + # precompiles or cheatcodes + if ( + # precompile + eq(to, con_addr(1)) + or eq(to, con_addr(4)) + # cheatcode calls + or eq(to, halmos_cheat_code.address) + or eq(to, hevm_cheat_code.address) + or eq(to, console.address) + ): + call_unknown() + return + + # known call target + to_addr = self.resolve_address_alias(ex, to) + if to_addr is not None: + call_known(to_addr) + return - # TODO: avoid relying directly on dict membership here - # it is based on hashing of the z3 expr objects rather than equivalence - if to in ex.code: - call_known() - else: + # simple ether transfer to unknown call target + if arg_size == 0: + call_unknown() + return + + # uninterpreted unknown calls + funsig = extract_funsig(arg) + if funsig in self.options["unknown_calls"]: + self.logs.add_uninterpreted_unknown_call(funsig, to, arg) call_unknown() + return + + raise HalmosException( + f"Unknown contract call: to = {hexify(to)}; " + f"calldata = {hexify(arg)}; callvalue = {hexify(fund)}" + ) def create( self, ex: Exec, + op: int, stack: List[Tuple[Exec, int]], step_id: int, - out: List[Exec], - bounded_loops: List[str], ) -> None: + if ex.message().is_static: + raise WriteInStaticContext(ex.context_str()) + value: Word = ex.st.pop() loc: int = int_of(ex.st.pop(), "symbolic CREATE offset") size: int = int_of(ex.st.pop(), "symbolic CREATE size") + if op == EVM.CREATE2: + salt = ex.st.pop() + + # lookup prank + caller = ex.prank.lookup(ex.this, con_addr(0)) + # contract creation code create_hexcode = wload(ex.st.memory, loc, size, prefer_concrete=True) create_code = Contract(create_hexcode) # new account address - new_addr = ex.new_address() + if op == EVM.CREATE: + new_addr = ex.new_address() + elif op == EVM.CREATE2: # EVM.CREATE2 + if isinstance(create_hexcode, bytes): + create_hexcode = con( + int.from_bytes(create_hexcode, "big"), len(create_hexcode) * 8 + ) + code_hash = ex.sha3_data(create_hexcode, create_hexcode.size() // 8) + hash_data = simplify(Concat(con(0xFF, 8), caller, salt, code_hash)) + new_addr = uint160(ex.sha3_data(hash_data, 85)) + else: + raise HalmosException(f"Unknown CREATE opcode: {op}") + + message = Message( + target=new_addr, + caller=caller, + value=value, + data=create_hexcode, + is_static=False, + call_scheme=op, + ) + + if new_addr in ex.code: + # address conflicts don't revert, they push 0 on the stack and continue + ex.st.push(con(0)) + ex.next_pc() + + # add a virtual subcontext to the trace for debugging purposes + subcall = CallContext(message=message, depth=ex.context.depth + 1) + subcall.output.data = b"" + subcall.output.error = AddressCollision() + ex.context.trace.append(subcall) + + stack.append((ex, step_id)) + return for addr in ex.code: - ex.solver.add(new_addr != addr) # ensure new address is fresh + ex.path.append(new_addr != addr) # ensure new address is fresh + + # backup current state + orig_code = ex.code.copy() + orig_storage = deepcopy(ex.storage) + orig_balance = ex.balance # setup new account - ex.code[new_addr] = create_code # existing code must be empty + ex.code[new_addr] = Contract(b"") # existing code must be empty ex.storage[new_addr] = {} # existing storage may not be empty and reset here - # lookup prank - caller = ex.prank.lookup(ex.this, new_addr) - # transfer value - # assume balance is enough; otherwise ignore this path - ex.solver.add(UGE(ex.balance_of(caller), value)) - if not (is_bv_value(value) and value.as_long() == 0): - ex.balance_update( - caller, self.arith(ex, EVM.SUB, ex.balance_of(caller), value) - ) - ex.balance_update( - new_addr, self.arith(ex, EVM.ADD, ex.balance_of(new_addr), value) - ) + self.transfer_value(ex, caller, new_addr, value) - # execute contract creation code - (new_exs, new_steps, new_bounded_loops) = self.run( - Exec( - code=ex.code, - storage=ex.storage, - balance=ex.balance, - # - block=ex.block, - # - calldata=[], - callvalue=value, - caller=ex.this, - this=new_addr, - # - pc=0, - st=State(), - jumpis={}, - output=None, - symbolic=False, - prank=Prank(), - # - solver=ex.solver, - path=ex.path, - # - log=ex.log, - cnts=ex.cnts, - sha3s=ex.sha3s, - storages=ex.storages, - balances=ex.balances, - calls=ex.calls, - failed=ex.failed, - error=ex.error, - ) - ) + def callback(new_ex, stack, step_id): + subcall = new_ex.context - bounded_loops.extend(new_bounded_loops) + # continue execution in the context of the parent + # pessimistic copy because the subcall results may diverge + new_ex.context = deepcopy(ex.context) + new_ex.context.trace.append(subcall) - # process result - for idx, new_ex in enumerate(new_exs): - # sanity checks - if new_ex.failed: - raise ValueError(new_ex) + new_ex.callback = ex.callback - opcode = new_ex.current_opcode() - if opcode in [EVM.STOP, EVM.RETURN]: - # new contract code - new_ex.code[new_addr] = Contract(new_ex.output) + new_ex.this = ex.this - # restore tx msg - new_ex.calldata = ex.calldata - new_ex.callvalue = ex.callvalue - new_ex.caller = ex.caller - new_ex.this = ex.this + # restore vm state + new_ex.pgm = ex.pgm + new_ex.pc = ex.pc + new_ex.st = deepcopy(ex.st) + new_ex.jumpis = deepcopy(ex.jumpis) + new_ex.symbolic = ex.symbolic + new_ex.prank = deepcopy(ex.prank) - # restore vm state - new_ex.pc = ex.pc - new_ex.st = deepcopy(ex.st) - new_ex.jumpis = deepcopy(ex.jumpis) - new_ex.output = None # output is reset, not restored - new_ex.symbolic = ex.symbolic - new_ex.prank = ex.prank + if subcall.is_stuck(): + # internal errors abort the current path, + yield new_ex + return + + elif subcall.output.error is None: + # new contract code, will revert if data is None + new_ex.code[new_addr] = Contract(subcall.output.data) # push new address to stack new_ex.st.push(uint256(new_addr)) - # add to worklist - new_ex.next_pc() - stack.append((new_ex, step_id)) else: # creation failed - out.append(new_ex) + new_ex.st.push(con(0)) + + # revert network states + new_ex.code = orig_code.copy() + new_ex.storage = deepcopy(orig_storage) + new_ex.balance = orig_balance + + # add to worklist + new_ex.next_pc() + stack.append((new_ex, step_id)) + + sub_ex = Exec( + code=ex.code, + storage=ex.storage, + balance=ex.balance, + # + block=ex.block, + # + context=CallContext(message=message, depth=ex.context.depth + 1), + callback=callback, + this=new_addr, + # + pgm=create_code, + pc=0, + st=State(), + jumpis={}, + symbolic=False, + prank=Prank(), + # + path=ex.path, + alias=ex.alias, + # + cnts=ex.cnts, + sha3s=ex.sha3s, + storages=ex.storages, + balances=ex.balances, + calls=ex.calls, + ) + + stack.append((sub_ex, step_id)) def jumpi( self, ex: Exec, stack: List[Tuple[Exec, int]], step_id: int, - bounded_loops: List[str], ) -> None: jid = ex.jumpi_id() @@ -1844,7 +2092,7 @@ def jumpi( follow_true = visited[True] < self.options["max_loop"] follow_false = visited[False] < self.options["max_loop"] if not (follow_true and follow_false): - bounded_loops.append(jid) + self.logs.bounded_loops.append(jid) else: # for constant-bounded loops follow_true = potential_true @@ -1858,14 +2106,12 @@ def jumpi( new_ex_true = self.create_branch(ex, cond_true, target) else: new_ex_true = ex - new_ex_true.solver.add(cond_true) - new_ex_true.path.append(str(cond_true)) + new_ex_true.path.append(cond_true, branching=True) new_ex_true.pc = target if follow_false: new_ex_false = ex - new_ex_false.solver.add(cond_false) - new_ex_false.path.append(str(cond_false)) + new_ex_false.path.append(cond_false, branching=True) new_ex_false.next_pc() if new_ex_true: @@ -1894,53 +2140,46 @@ def jump(self, ex: Exec, stack: List[Tuple[Exec, int]], step_id: int) -> None: # otherwise, create a new execution for feasible targets elif self.options["sym_jump"]: - for target in ex.code[ex.this].valid_jump_destinations(): + for target in ex.pgm.valid_jump_destinations(): target_reachable = simplify(dst == target) if ex.check(target_reachable) != unsat: # jump if self.options.get("debug"): - print(f"We can jump to {target} with model {ex.solver.model()}") + print( + f"We can jump to {target} with model {ex.path.solver.model()}" + ) new_ex = self.create_branch(ex, target_reachable, target) stack.append((new_ex, step_id)) else: raise NotConcreteError(f"symbolic JUMP target: {dst}") def create_branch(self, ex: Exec, cond: BitVecRef, target: int) -> Exec: - new_solver = SolverFor("QF_AUFBV") - new_solver.set(timeout=self.options["timeout"]) - new_solver.add(ex.solver.assertions()) - new_solver.add(cond) - new_path = deepcopy(ex.path) - new_path.append(str(cond)) + new_path = ex.path.branch(cond) new_ex = Exec( code=ex.code.copy(), # shallow copy for potential new contract creation; existing code doesn't change storage=deepcopy(ex.storage), - balance=deepcopy(ex.balance), + balance=ex.balance, # block=deepcopy(ex.block), # - calldata=ex.calldata, - callvalue=ex.callvalue, - caller=ex.caller, + context=deepcopy(ex.context), + callback=ex.callback, this=ex.this, # + pgm=ex.pgm, pc=target, st=deepcopy(ex.st), jumpis=deepcopy(ex.jumpis), - output=deepcopy(ex.output), symbolic=ex.symbolic, prank=deepcopy(ex.prank), # - solver=new_solver, path=new_path, + alias=ex.alias.copy(), # - log=deepcopy(ex.log), cnts=deepcopy(ex.cnts), - sha3s=deepcopy(ex.sha3s), - storages=deepcopy(ex.storages), - balances=deepcopy(ex.balances), - calls=deepcopy(ex.calls), - failed=ex.failed, - error=ex.error, + sha3s=ex.sha3s.copy(), + storages=ex.storages.copy(), + balances=ex.balances.copy(), + calls=ex.calls.copy(), ) return new_ex @@ -1960,24 +2199,32 @@ def gen_nested_ite(curr: int) -> BitVecRef: # If(idx == 0, Extract(255, 248, w), If(idx == 1, Extract(247, 240, w), ..., If(idx == 31, Extract(7, 0, w), 0)...)) return ZeroExt(248, gen_nested_ite(0)) - def run(self, ex0: Exec) -> Tuple[List[Exec], Steps]: - out: List[Exec] = [] - bounded_loops: List[str] = [] - steps: Steps = {} + def run(self, ex0: Exec) -> Iterator[Exec]: step_id: int = 0 stack: List[Tuple[Exec, int]] = [(ex0, 0)] + + def finalize(ex: Exec): + # if it's at the top-level, there is no callback; yield the current execution state + if ex.callback is None: + yield ex + + # otherwise, execute the callback to return to the parent execution context + # note: `yield from` is used as the callback may yield the current execution state that got stuck + else: + yield from ex.callback(ex, stack, step_id) + while stack: try: - if ( - "max_width" in self.options - and len(out) >= self.options["max_width"] - ): - break - (ex, prev_step_id) = stack.pop() step_id += 1 + if not ex.path.is_activated(): + ex.path.activate() + + if ex.context.depth > MAX_CALL_DEPTH: + raise MessageDepthLimitError(ex.context) + insn = ex.current_instruction() opcode = insn.opcode ex.cnts["opcode"][opcode] += 1 @@ -1988,40 +2235,36 @@ def run(self, ex0: Exec) -> Tuple[List[Exec], Steps]: ): continue + # TODO: clean up if self.options.get("log"): if opcode == EVM.JUMPI: - steps[step_id] = {"parent": prev_step_id, "exec": str(ex)} + self.steps[step_id] = {"parent": prev_step_id, "exec": str(ex)} # elif opcode == EVM.CALL: - # steps[step_id] = {'parent': prev_step_id, 'exec': str(ex) + ex.st.str_memory() + '\n'} + # self.steps[step_id] = {'parent': prev_step_id, 'exec': str(ex) + ex.st.str_memory() + '\n'} else: - # steps[step_id] = {'parent': prev_step_id, 'exec': ex.summary()} - steps[step_id] = {"parent": prev_step_id, "exec": str(ex)} + # self.steps[step_id] = {'parent': prev_step_id, 'exec': ex.summary()} + self.steps[step_id] = {"parent": prev_step_id, "exec": str(ex)} if self.options.get("print_steps"): print(ex) - if opcode == EVM.STOP: - ex.output = None - out.append(ex) - continue - - elif opcode == EVM.INVALID: - ex.output = None - out.append(ex) - continue - - elif opcode == EVM.REVERT: - ex.output = ex.st.ret() - out.append(ex) - continue + if opcode in [EVM.STOP, EVM.INVALID, EVM.REVERT, EVM.RETURN]: + if opcode == EVM.STOP: + ex.halt() + elif opcode == EVM.INVALID: + ex.halt(error=InvalidOpcode(opcode)) + elif opcode == EVM.REVERT: + ex.halt(data=ex.st.ret(), error=Revert()) + elif opcode == EVM.RETURN: + ex.halt(data=ex.st.ret()) + else: + raise ValueError(opcode) - elif opcode == EVM.RETURN: - ex.output = ex.st.ret() - out.append(ex) + yield from finalize(ex) continue elif opcode == EVM.JUMPI: - self.jumpi(ex, stack, step_id, bounded_loops) + self.jumpi(ex, stack, step_id) continue elif opcode == EVM.JUMP: @@ -2078,12 +2321,10 @@ def run(self, ex0: Exec) -> Tuple[List[Exec], Steps]: elif opcode == EVM.ISZERO: ex.st.push(is_zero(ex.st.pop())) - elif opcode == EVM.AND: - ex.st.push(and_of(ex.st.pop(), ex.st.pop())) - elif opcode == EVM.OR: - ex.st.push(or_of(ex.st.pop(), ex.st.pop())) + elif opcode in [EVM.AND, EVM.OR, EVM.XOR]: + ex.st.push(bitwise(opcode, ex.st.pop(), ex.st.pop())) elif opcode == EVM.NOT: - ex.st.push(~ex.st.pop()) # bvnot + ex.st.push(~b2i(ex.st.pop())) # bvnot elif opcode == EVM.SHL: w = ex.st.pop() ex.st.push(b2i(ex.st.pop()) << b2i(w)) # bvshl @@ -2100,52 +2341,57 @@ def run(self, ex0: Exec) -> Tuple[List[Exec], Steps]: bl = (w + 1) * 8 ex.st.push(SignExt(256 - bl, Extract(bl - 1, 0, ex.st.pop()))) - elif opcode == EVM.XOR: - ex.st.push(ex.st.pop() ^ ex.st.pop()) # bvxor - elif opcode == EVM.CALLDATALOAD: - if ex.calldata is None: + calldata = ex.calldata() + if calldata is None: ex.st.push(f_calldataload(ex.st.pop())) else: - offset: int = int_of( - ex.st.pop(), "symbolic CALLDATALOAD offset" - ) - ex.st.push( - Concat( - (ex.calldata + [BitVecVal(0, 8)] * 32)[ - offset : offset + 32 - ] - ) - ) + err_msg = "symbolic CALLDATALOAD offset" + offset: int = int_of(ex.st.pop(), err_msg) + data = padded_slice(calldata, offset, 32, default=con(0, 8)) + ex.st.push(Concat(data)) + elif opcode == EVM.CALLDATASIZE: - if ex.calldata is None: - ex.st.push(f_calldatasize()) - else: - ex.st.push(con(len(ex.calldata))) + cd = ex.calldata() + + # TODO: is optional calldata necessary? + ex.st.push(f_calldatasize() if cd is None else con(len(cd))) + elif opcode == EVM.CALLVALUE: - ex.st.push(ex.callvalue) + ex.st.push(ex.callvalue()) elif opcode == EVM.CALLER: - ex.st.push(uint256(ex.caller)) + ex.st.push(uint256(ex.caller())) elif opcode == EVM.ORIGIN: ex.st.push(uint256(f_origin())) elif opcode == EVM.ADDRESS: ex.st.push(uint256(ex.this)) + # TODO: define f_extcodesize for known addresses in advance elif opcode == EVM.EXTCODESIZE: - address = uint160(ex.st.pop()) - if address in ex.code: - codesize = con(len(ex.code[address])) + account = uint160(ex.st.pop()) + account_addr = self.resolve_address_alias(ex, account) + if account_addr is not None: + codesize = con(len(ex.code[account_addr])) else: - codesize = f_extcodesize(address) + codesize = f_extcodesize(account) if ( - address == hevm_cheat_code.address - or address == halmos_cheat_code.address + eq(account, hevm_cheat_code.address) + or eq(account, halmos_cheat_code.address) + or eq(account, console.address) ): - ex.solver.add(codesize > 0) + ex.path.append(codesize > 0) ex.st.push(codesize) + # TODO: define f_extcodehash for known addresses in advance elif opcode == EVM.EXTCODEHASH: - ex.st.push(f_extcodehash(ex.st.pop())) + account = uint160(ex.st.pop()) + account_addr = self.resolve_address_alias(ex, account) + codehash = ( + f_extcodehash(account_addr) + if account_addr is not None + else f_extcodehash(account) + ) + ex.st.push(codehash) elif opcode == EVM.CODESIZE: - ex.st.push(con(len(ex.code[ex.this]))) + ex.st.push(con(len(ex.pgm))) elif opcode == EVM.GAS: ex.st.push(f_gas(con(ex.new_gas_id()))) elif opcode == EVM.GASPRICE: @@ -2177,15 +2423,20 @@ def run(self, ex0: Exec) -> Tuple[List[Exec], Steps]: elif opcode == EVM.SELFBALANCE: ex.st.push(ex.balance_of(ex.this)) - elif opcode == EVM.CALL or opcode == EVM.STATICCALL: - self.call(ex, opcode, stack, step_id, out, bounded_loops) + elif opcode in [ + EVM.CALL, + EVM.CALLCODE, + EVM.DELEGATECALL, + EVM.STATICCALL, + ]: + self.call(ex, opcode, stack, step_id) continue elif opcode == EVM.SHA3: ex.sha3() - elif opcode == EVM.CREATE: - self.create(ex, stack, step_id, out, bounded_loops) + elif opcode in [EVM.CREATE, EVM.CREATE2]: + self.create(ex, opcode, stack, step_id) continue elif opcode == EVM.POP: @@ -2204,9 +2455,9 @@ def run(self, ex0: Exec) -> Tuple[List[Exec], Steps]: ex.st.push(con(size)) elif opcode == EVM.SLOAD: - ex.st.push(ex.sload(ex.this, ex.st.pop())) + ex.st.push(self.sload(ex, ex.this, ex.st.pop())) elif opcode == EVM.SSTORE: - ex.sstore(ex.this, ex.st.pop(), ex.st.pop()) + self.sstore(ex, ex.this, ex.st.pop(), ex.st.pop()) elif opcode == EVM.RETURNDATASIZE: ex.st.push(con(ex.returndatasize())) @@ -2215,8 +2466,15 @@ def run(self, ex0: Exec) -> Tuple[List[Exec], Steps]: offset: int = int_of(ex.st.pop(), "symbolic RETURNDATACOPY offset") # size (in bytes) size: int = int_of(ex.st.pop(), "symbolic RETURNDATACOPY size") + + # TODO: do we need to pass returndatasize here? wstore_partial( - ex.st.memory, loc, offset, size, ex.output, ex.returndatasize() + ex.st.memory, + loc, + offset, + size, + ex.returndata(), + ex.returndatasize(), ) elif opcode == EVM.CALLDATACOPY: @@ -2225,32 +2483,18 @@ def run(self, ex0: Exec) -> Tuple[List[Exec], Steps]: # size (in bytes) size: int = int_of(ex.st.pop(), "symbolic CALLDATACOPY size") if size > 0: - if ex.calldata is None: + calldata = ex.message().data + if calldata is None: f_calldatacopy = Function( "calldatacopy_" + str(size * 8), - BitVecSort(256), - BitVecSort(size * 8), + BitVecSort256, + BitVecSorts[size * 8], ) data = f_calldatacopy(offset) wstore(ex.st.memory, loc, size, data) else: - if offset + size <= len(ex.calldata): - wstore_bytes( - ex.st.memory, - loc, - size, - ex.calldata[offset : offset + size], - ) - elif offset == len(ex.calldata): - # copy zero bytes - wstore_bytes( - ex.st.memory, - loc, - size, - [BitVecVal(0, 8) for _ in range(size)], - ) - else: - raise ValueError(offset, size, len(ex.calldata)) + data = padded_slice(calldata, offset, size, con(0, 8)) + wstore_bytes(ex.st.memory, loc, size, data) elif opcode == EVM.CODECOPY: loc: int = ex.st.mloc() @@ -2259,7 +2503,14 @@ def run(self, ex0: Exec) -> Tuple[List[Exec], Steps]: size: int = int_of(ex.st.pop(), "symbolic CODECOPY size") wextend(ex.st.memory, loc, size) - codeslice = ex.code[ex.this][offset : offset + size] + codeslice = ex.pgm.slice(offset, offset + size) + + actual_size = byte_length(codeslice) + if actual_size != size: + raise HalmosException( + f"CODECOPY: expected {size} bytes but got {actual_size}" + ) + ex.st.memory[loc : loc + size] = iter_bytes(codeslice) elif opcode == EVM.BYTE: @@ -2285,16 +2536,16 @@ def run(self, ex0: Exec) -> Tuple[List[Exec], Steps]: ex.st.push(self.sym_byte_of(idx, w)) elif EVM.LOG0 <= opcode <= EVM.LOG4: - num_keys: int = opcode - EVM.LOG0 + if ex.message().is_static: + raise WriteInStaticContext(ex.context_str()) + + num_topics: int = opcode - EVM.LOG0 loc: int = ex.st.mloc() - # size (in bytes) size: int = int_of(ex.st.pop(), "symbolic LOG data size") - keys = [] - for _ in range(num_keys): - keys.append(ex.st.pop()) - ex.log.append( - (keys, wload(ex.st.memory, loc, size) if size > 0 else None) - ) + topics = list(ex.st.pop() for _ in range(num_topics)) + data = wload(ex.st.memory, loc, size) if size > 0 else None + + ex.emit_log(EventLog(ex.this, topics, data)) elif opcode == EVM.PUSH0: ex.st.push(con(0)) @@ -2304,7 +2555,7 @@ def run(self, ex0: Exec) -> Tuple[List[Exec], Steps]: val = int_of(insn.operand) if opcode == EVM.PUSH32 and val in sha3_inv: # restore precomputed hashes - ex.sha3_data(con(sha3_inv[val]), 32) + ex.st.push(ex.sha3_data(con(sha3_inv[val]), 32)) else: ex.st.push(con(val)) else: @@ -2318,23 +2569,25 @@ def run(self, ex0: Exec) -> Tuple[List[Exec], Steps]: ex.st.swap(opcode - EVM.SWAP1 + 1) else: - out.append(ex) - continue + # TODO: switch to InvalidOpcode when we have full opcode coverage + # this halts the path, but we should only halt the current context + raise HalmosException(f"Unsupported opcode {hex(opcode)}") ex.next_pc() stack.append((ex, step_id)) - except NotConcreteError as err: - ex.error = f"{err}" - out.append(ex) + except EvmException as err: + ex.halt(error=err) + yield from finalize(ex) continue - except Exception as err: + except HalmosException as err: if self.options["debug"]: - print(ex) - raise + print(err) - return (out, steps, bounded_loops) + ex.halt(data=None, error=err) + yield from finalize(ex) + continue def mk_exec( self, @@ -2345,29 +2598,13 @@ def mk_exec( # block, # - calldata, - callvalue, - caller, + context: CallContext, + # this, # - # pc, - # st, - # jumpis, - # output, + pgm, symbolic, - # prank, - # - solver, - # path, - # - # log, - # cnts, - # sha3s, - # storages, - # balances, - # calls, - # failed, - # error, + path, ) -> Exec: return Exec( code=code, @@ -2376,27 +2613,24 @@ def mk_exec( # block=block, # - calldata=calldata, - callvalue=callvalue, - caller=caller, - this=this, + context=context, + callback=None, # top-level; no callback # + this=this, + pgm=pgm, pc=0, st=State(), jumpis={}, - output=None, symbolic=symbolic, prank=Prank(), # - solver=solver, - path=[], + path=path, + alias={}, # log=[], cnts=defaultdict(lambda: defaultdict(int)), - sha3s=[], + sha3s={}, storages={}, balances={}, calls=[], - failed=False, - error="", ) diff --git a/src/halmos/utils.py b/src/halmos/utils.py index 020cb4af..f892ea77 100644 --- a/src/halmos/utils.py +++ b/src/halmos/utils.py @@ -2,15 +2,241 @@ import re -from typing import Dict, Tuple +from timeit import default_timer as timer +from typing import Dict, Tuple, Any, Optional, Union as UnionType from z3 import * +from .exceptions import NotConcreteError, HalmosException + + +Word = Any # z3 expression (including constants) +Byte = Any # z3 expression (including constants) +Bytes = Any # z3 expression (including constants) +Address = BitVecRef # 160-bitvector + + +# dynamic BitVecSort sizes +class BitVecSortCache: + def __init__(self): + self.cache = {} + for size in ( + 1, + 8, + 16, + 32, + 64, + 128, + 160, + 256, + 264, + 288, + 512, + 544, + 800, + 1024, + 1056, + ): + self.cache[size] = BitVecSort(size) + + def __getitem__(self, size: int) -> BitVecSort: + hit = self.cache.get(size) + return hit if hit is not None else BitVecSort(size) + + +BitVecSorts = BitVecSortCache() + +# known, fixed BitVecSort sizes +BitVecSort1 = BitVecSorts[1] +BitVecSort8 = BitVecSorts[8] +BitVecSort160 = BitVecSorts[160] +BitVecSort256 = BitVecSorts[256] +BitVecSort264 = BitVecSorts[264] +BitVecSort512 = BitVecSorts[512] + + +def concat(args): + if len(args) > 1: + return Concat(args) + else: + return args[0] + + +def uint256(x: BitVecRef) -> BitVecRef: + bitsize = x.size() + if bitsize > 256: + raise ValueError(x) + if bitsize == 256: + return x + return simplify(ZeroExt(256 - bitsize, x)) + + +def int256(x: BitVecRef) -> BitVecRef: + bitsize = x.size() + if bitsize > 256: + raise ValueError(x) + if bitsize == 256: + return x + return simplify(SignExt(256 - bitsize, x)) + + +def uint160(x: BitVecRef) -> BitVecRef: + bitsize = x.size() + if bitsize > 256: + raise ValueError(x) + if bitsize == 160: + return x + if bitsize > 160: + return simplify(Extract(159, 0, x)) + else: + return simplify(ZeroExt(160 - bitsize, x)) + + +def con(n: int, size_bits=256) -> Word: + return BitVecVal(n, BitVecSorts[size_bits]) + + +# x == b if sort(x) = bool +# int_to_bool(x) == b if sort(x) = int +def test(x: Word, b: bool) -> Word: + if is_bool(x): + if b: + return x + else: + return Not(x) + elif is_bv(x): + if b: + return x != con(0) + else: + return x == con(0) + else: + raise ValueError(x) + + +def is_non_zero(x: Word) -> Word: + return test(x, True) + + +def is_zero(x: Word) -> Word: + return test(x, False) + + +def create_solver(logic="QF_AUFBV", ctx=None, timeout=0, max_memory=0): + # QF_AUFBV: quantifier-free bitvector + array theory: https://smtlib.cs.uiowa.edu/logics.shtml + solver = SolverFor(logic, ctx=ctx) + + # set timeout + solver.set(timeout=timeout) + + # set memory limit + if max_memory > 0: + solver.set(max_memory=max_memory) + + return solver + + +def extract_bytes_argument(calldata: BitVecRef, arg_idx: int) -> bytes: + """Extracts idx-th argument of string from calldata""" + offset = int_of( + extract_bytes(calldata, 4 + arg_idx * 32, 32), + "symbolic offset for bytes argument", + ) + length = int_of( + extract_bytes(calldata, 4 + offset, 32), + "symbolic size for bytes argument", + ) + if length == 0: + return b"" + + bytes = extract_bytes(calldata, 4 + offset + 32, length) + return bv_value_to_bytes(bytes) if is_bv_value(bytes) else bytes + + +def extract_string_argument(calldata: BitVecRef, arg_idx: int): + """Extracts idx-th argument of string from calldata""" + string_bytes = extract_bytes_argument(calldata, arg_idx) + return string_bytes.decode("utf-8") if string_bytes else "" + + +def extract_bytes(data: BitVecRef, byte_offset: int, size_bytes: int) -> BitVecRef: + """Extract bytes from calldata. Zero-pad if out of bounds.""" + n = data.size() + if n % 8 != 0: + raise ValueError(n) + + # will extract hi - lo + 1 bits + hi = n - 1 - byte_offset * 8 + lo = n - byte_offset * 8 - size_bytes * 8 + lo = 0 if lo < 0 else lo + + val = simplify(Extract(hi, lo, data)) + + zero_padding = size_bytes * 8 - val.size() + if zero_padding < 0: + raise ValueError(val) + if zero_padding > 0: + val = simplify(Concat(val, con(0, zero_padding))) + + return val + + +def extract_funsig(calldata: BitVecRef): + """Extracts the function signature (first 4 bytes) from calldata""" + return extract_bytes(calldata, 0, 4) + def bv_value_to_bytes(x: BitVecNumRef) -> bytes: - if x.size() % 8 != 0: - raise ValueError(x, x.size()) - return x.as_long().to_bytes(x.size() // 8, "big") + return x.as_long().to_bytes(byte_length(x, strict=True), "big") + + +def unbox_int(x: Any) -> Any: + """ + Attempts to convert int-like objects to int + """ + if isinstance(x, bytes): + return int.from_bytes(x, "big") + + if is_bv_value(x): + return x.as_long() + + return x + + +def int_of(x: Any, err: str = "expected concrete value but got") -> int: + """ + Converts int-like objects to int or raises NotConcreteError + """ + res = unbox_int(x) + + if isinstance(res, int): + return res + + raise NotConcreteError(f"{err}: {x}") + + +def byte_length(x: Any, strict=True) -> int: + if is_bv(x): + if x.size() % 8 != 0 and strict: + raise HalmosException(f"byte_length({x}) with bit size {x.size()}") + return math.ceil(x.size() / 8) + + if isinstance(x, bytes): + return len(x) + + raise HalmosException(f"byte_length({x}) of type {type(x)}") + + +def stripped(hexstring: str) -> str: + """Remove 0x prefix from hexstring""" + return hexstring[2:] if hexstring.startswith("0x") else hexstring + + +def decode_hex(hexstring: str) -> Optional[bytes]: + try: + # not checking if length is even because fromhex accepts spaces + return bytes.fromhex(stripped(hexstring)) + except ValueError: + return None def hexify(x): @@ -21,13 +247,97 @@ def hexify(x): elif isinstance(x, bytes): return "0x" + x.hex() elif is_bv_value(x): - # preserving bitsize could be confusing due to some bv values given as strings; need refactoring to fix properly - # return hexify(x.as_long().to_bytes((x.size() + 7) // 8, 'big')) # bitsize may not be a multiple of 8 - return hex(x.as_long()) + # maintain the byte size of x + num_bytes = byte_length(x, strict=False) + return f"0x{x.as_long():0{num_bytes * 2}x}" + elif is_app(x): + return f"{str(x.decl())}({', '.join(map(hexify, x.children()))})" else: return hexify(str(x)) +def render_uint(x: BitVecRef) -> str: + if is_bv_value(x): + val = int_of(x) + return f"0x{val:0{byte_length(x, strict=False) * 2}x} ({val})" + + return hexify(x) + + +def render_int(x: BitVecRef) -> str: + if is_bv_value(x): + val = x.as_signed_long() + return f"0x{x.as_long():0{byte_length(x, strict=False) * 2}x} ({val})" + + return hexify(x) + + +def render_bool(b: BitVecRef) -> str: + return str(b.as_long() != 0).lower() if is_bv_value(b) else hexify(b) + + +def render_string(s: BitVecRef) -> str: + str_val = bytes.fromhex(stripped(hexify(s))).decode("utf-8") + return f'"{str_val}"' + + +def render_bytes(b: UnionType[BitVecRef, bytes]) -> str: + if is_bv(b): + return hexify(b) + f" ({byte_length(b, strict=False)} bytes)" + else: + return f'hex"{stripped(b.hex())}"' + + +def render_address(a: BitVecRef) -> str: + if is_bv_value(a): + return f"0x{a.as_long():040x}" + + return hexify(a) + + +def stringify(symbol_name: str, val: Any): + """ + Formats a value based on the inferred type of the variable. + + Expects symbol_name to be of the form 'p__', e.g. 'p_x_uint256' + """ + if not is_bv_value(val): + warn(f"{val} is not a bitvector value") + return hexify(val) + + tokens = symbol_name.split("_") + if len(tokens) < 3: + warn(f"Failed to infer type for symbol '{symbol_name}'") + return hexify(val) + + if len(tokens) >= 4 and tokens[-1].isdigit(): + # we may have something like p_val_bytes_01 + # the last token being a symbol number, discard it + tokens.pop() + + type_name = tokens[-1] + + try: + if type_name.startswith("uint"): + return render_uint(val) + elif type_name.startswith("int"): + return render_int(val) + elif type_name == "bool": + return render_bool(val) + elif type_name == "string": + return render_string(val) + elif type_name == "bytes": + return render_bytes(val) + elif type_name == "address": + return render_address(val) + else: # bytes32, bytes4, structs, etc. + return hexify(val) + except Exception as e: + # log error and move on + warn(f"Failed to stringify {val} of type {type_name}: {repr(e)}") + return hexify(val) + + def assert_address(x: BitVecRef) -> None: if x.size() != 160: raise ValueError(x) @@ -46,12 +356,64 @@ def con_addr(n: int) -> BitVecRef: return BitVecVal(n, 160) +def green(text: str) -> str: + return f"\033[32m{text}\033[0m" + + +def red(text: str) -> str: + return f"\033[31m{text}\033[0m" + + +def yellow(text: str) -> str: + return f"\033[33m{text}\033[0m" + + +def cyan(text: str) -> str: + return f"\033[36m{text}\033[0m" + + +def magenta(text: str) -> str: + return f"\033[35m{text}\033[0m" + + def color_good(text: str) -> str: - return "\033[32m" + text + "\033[0m" + return green(text) + + +def color_error(text: str) -> str: + return red(text) def color_warn(text: str) -> str: - return "\033[31m" + text + "\033[0m" + return red(text) + + +def color_info(text: str) -> str: + return cyan(text) + + +def color_debug(text: str) -> str: + return magenta(text) + + +def error(text: str) -> None: + print(color_error(text)) + + +def warn(text: str) -> None: + print(color_warn(text)) + + +def info(text: str) -> None: + print(color_info(text)) + + +def debug(text: str) -> None: + print(color_debug(text)) + + +def indent_text(text: str, n: int = 4) -> str: + return "\n".join(" " * n + line for line in text.splitlines()) class EVM: @@ -626,3 +988,70 @@ def mk_sha3_inv_offset(m: Dict[int, int]) -> Dict[int, Tuple[int, int]]: } sha3_inv_offset: Dict[int, Tuple[int, int]] = mk_sha3_inv_offset(sha3_inv) + + +class NamedTimer: + def __init__(self, name: str, auto_start=True): + self.name = name + self.start_time = timer() if auto_start else None + self.end_time = None + self.sub_timers = [] + + def start(self): + if self.start_time is not None: + raise ValueError(f"Timer {self.name} has already been started.") + self.start_time = timer() + + def stop(self, stop_subtimers=True): + if stop_subtimers: + for sub_timer in self.sub_timers: + sub_timer.stop() + + # if the timer has already been stopped, do nothing + self.end_time = self.end_time or timer() + + def create_subtimer(self, name, auto_start=True, stop_previous=True): + for timer in self.sub_timers: + if timer.name == name: + raise ValueError(f"Timer with name {name} already exists.") + + if stop_previous and self.sub_timers: + self.sub_timers[-1].stop() + + sub_timer = NamedTimer(name, auto_start=auto_start) + self.sub_timers.append(sub_timer) + return sub_timer + + def __getitem__(self, name): + for timer in self.sub_timers: + if timer.name == name: + return timer + raise ValueError(f"Timer with name {name} does not exist.") + + def elapsed(self) -> float: + if self.start_time is None: + raise ValueError(f"Timer {self.name} has not been started") + + end_time = self.end_time if self.end_time is not None else timer() + + return end_time - self.start_time + + def report(self, include_subtimers=True) -> str: + sub_reports_str = "" + + if include_subtimers: + sub_reports = [ + f"{timer.name}: {timer.elapsed():.2f}s" for timer in self.sub_timers + ] + sub_reports_str = f" ({', '.join(sub_reports)})" if sub_reports else "" + + return f"{self.name}: {self.elapsed():.2f}s{sub_reports_str}" + + def __str__(self): + return self.report() + + def __repr__(self): + return ( + f"NamedTimer(name={self.name}, start_time={self.start_time}, " + f"end_time={self.end_time}, sub_timers={self.sub_timers})" + ) diff --git a/src/halmos/warnings.py b/src/halmos/warnings.py index 6f98b16c..b589307e 100644 --- a/src/halmos/warnings.py +++ b/src/halmos/warnings.py @@ -20,8 +20,13 @@ def url(self) -> str: COUNTEREXAMPLE_INVALID = ErrorCode("counterexample-invalid") COUNTEREXAMPLE_UNKNOWN = ErrorCode("counterexample-unknown") +INTERNAL_ERROR = ErrorCode("internal-error") +UNSUPPORTED_OPCODE = ErrorCode("unsupported-opcode") +LIBRARY_PLACEHOLDER = ErrorCode("library-placeholder") +REVERT_ALL = ErrorCode("revert-all") LOOP_BOUND = ErrorCode("loop-bound") +UNINTERPRETED_UNKNOWN_CALLS = ErrorCode("uninterpreted-unknown-calls") def warn(error_code: ErrorCode, msg: str): diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..8932b6f8 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,7 @@ +def pytest_addoption(parser): + parser.addoption( + "--halmos-options", + metavar="OPTIONS", + default="", + help="Halmos commandline options", + ) diff --git a/tests/expected/all.json b/tests/expected/all.json new file mode 100644 index 00000000..828cbd8d --- /dev/null +++ b/tests/expected/all.json @@ -0,0 +1,1919 @@ +{ + "exitcode": 1, + "test_results": { + "test/Invalid.t.sol:OldCompilerTest": [ + { + "name": "check_assert(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_myAssert(uint256)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Arith.t.sol:ArithTest": [ + { + "name": "check_Exp(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_Mod(uint256,uint256,address)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/AssertTest.t.sol:AssertTest": [ + { + "name": "check_assert_not_propagated()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_fail_propagated()", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Block.t.sol:BlockCheatCodeTest": [ + { + "name": "check_chainId(uint64)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_coinbase(address)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_difficulty(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_fee(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_roll(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_warp(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Buffers.t.sol:BuffersTest": [ + { + "name": "check_calldatacopy_large_offset()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_calldataload_large_offset()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_codecopy_large_offset()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_codecopy_offset_across_boundary()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Byte.t.sol:ByteTest": [ + { + "name": "check_byte(uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Byte.t.sol:SymbolicByteTest": [ + { + "name": "check_SymbolicByteIndex(uint8,uint8)", + "exitcode": 1, + "num_models": 2, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Call.t.sol:CallTest": [ + { + "name": "check_call(uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_callcode(uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_delegatecall(uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_staticcall(uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/CallAlias.t.sol:CallAliasTest": [ + { + "name": "check_alias_1(address,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_alias_1a(bool,address,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_alias_2(address,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_alias_2a(bool,address,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_alias_3(address,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_alias_3a(bool,address,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Console.t.sol:ConsoleTest": [ + { + "name": "check_log_address()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_log_bool()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_log_bytes()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_log_bytes32()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_log_int()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_log_string()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_log_uint()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_log_undecodable_string()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_log_unsupported()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Const.t.sol:ConstTest": [ + { + "name": "check_Const()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Const.t.sol:ConstTestTest": [ + { + "name": "check_Const()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Constructor.t.sol:ConstructorTest": [ + { + "name": "check_constructor()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_setCodesize()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Context.t.sol:ContextTest": [ + { + "name": "check_call0_fail()", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_call0_halmos_exception()", + "exitcode": 3, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_call0_normal(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_call1_fail(uint256)", + "exitcode": 1, + "num_models": 2, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_call1_halmos_exception(uint256)", + "exitcode": 3, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_call1_normal(uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_create()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_create_halmos_exception()", + "exitcode": 3, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_returndata()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_setup()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Counter.t.sol:CounterTest": [ + { + "name": "check_div_1(uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_div_2(uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_foo(uint256,uint256,uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_inc()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_incBy(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_incOpt()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_loopConst()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_loopConstIf()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_loopDoWhile(uint8)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_loopFor(uint8)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_loopWhile(uint8)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_mulDiv(uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_set(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_setString(uint256,string,uint256,string,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_setSum(uint248,uint248)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Create.t.sol:CreateTest": [ + { + "name": "check_const()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_immutable()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_initialized()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_set(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Create2.t.sol:Create2Test": [ + { + "name": "check_create2(uint256,uint256,bytes32)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_create2_caller(address,uint256,uint256,bytes32)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_create2_collision_alias(uint256,uint256,bytes32)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_create2_collision_basic(uint256,uint256,bytes32)", + "exitcode": 4, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_create2_collision_lowlevel(uint256,uint256,bytes32)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_create2_concrete()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_create2_no_collision_1(uint256,uint256,bytes32,bytes32)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_create2_no_collision_2(uint256,uint256,bytes32)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Deal.t.sol:DealTest": [ + { + "name": "check_deal_1(address,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_deal_2(address,uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_deal_new()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Foundry.t.sol:FoundryTest": [ + { + "name": "check_assume(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_deployCode(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_etch_Concrete()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_etch_Overwrite()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Getter.t.sol:GetterTest": [ + { + "name": "check_Getter(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_externalGetter(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/HalmosCheatCode.t.sol:HalmosCheatCodeTest": [ + { + "name": "check_FailUnknownCheatcode()", + "exitcode": 3, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_SymbolLabel()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_createAddress()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_createBool()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_createBytes()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_createBytes32()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_createBytes4()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_createInt()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_createInt256()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_createString()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_createUint()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_createUint256()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Library.t.sol:LibraryTest": [ + { + "name": "check_add(uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/LibraryLinking.t.sol:LibTest": [ + { + "name": "check_foo()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/LibraryLinking.t.sol:LibTest2": [ + { + "name": "check_bar()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/List.t.sol:ListTest": [ + { + "name": "check_add(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_remove()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_set(uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/List.t.sol:ListTestTest": [ + { + "name": "check_add(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_remove()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_set(uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Natspec.t.sol:NatspecTestContract": [ + { + "name": "check_Loop3(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_Loop3Fail(uint256)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Natspec.t.sol:NatspecTestFunction": [ + { + "name": "check_Loop2(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_Loop2Fail(uint256)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_Loop3(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_Loop3Fail(uint256)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Natspec.t.sol:NatspecTestNone": [ + { + "name": "check_Loop2(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_Loop2Fail(uint256)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Natspec.t.sol:NatspecTestOverwrite": [ + { + "name": "check_Loop3(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_Loop3Fail(uint256)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_Loop4(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_Loop4Fail(uint256)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Natspec.t.sol:NatspecTestSetup": [ + { + "name": "check_Loop2(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_Loop2Fail(uint256)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Opcode.t.sol:OpcodeTest": [ + { + "name": "check_PUSH0()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_SIGNEXTEND(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Prank.t.sol:PrankSetUpTest": [ + { + "name": "check_prank(address)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Prank.t.sol:PrankTest": [ + { + "name": "check_prank(address)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_prank_Constructor(address)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_prank_External(address)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_prank_ExternalSelf(address)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_prank_Internal(address)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_prank_New(address)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_prank_Reset1(address)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_prank_Reset2(address)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_startPrank(address)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_stopPrank_1(address)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_stopPrank_2()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Proxy.t.sol:ProxyTest": [ + { + "name": "check_foo(uint256,uint256,address)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Reset.t.sol:ResetTest": [ + { + "name": "check_foo()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Revert.t.sol:CTest": [ + { + "name": "check_Revert1(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_Revert2(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_RevertBalance(bool,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_RevertCode(address)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Send.t.sol:SendTest": [ + { + "name": "check_create(address,uint256,address,bytes32,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_send(address,uint256,address,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_transfer(address,uint256,address)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Setup.t.sol:SetupFailTest": [], + "test/Setup.t.sol:SetupTest": [ + { + "name": "check_True()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/SetupPlus.t.sol:SetupPlusTest": [ + { + "name": "check_Setup()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/SetupPlus.t.sol:SetupPlusTestB": [ + { + "name": "check_Setup()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/SetupSymbolic.t.sol:SetupSymbolicTest": [ + { + "name": "check_True()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Sha3.t.sol:Sha3Test": [ + { + "name": "check_hash()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_no_hash_collision_assumption()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/SignExtend.t.sol:SignExtendTest": [ + { + "name": "check_SIGNEXTEND(int16)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Signature.t.sol:SignatureTest": [ + { + "name": "check_ecrecover(bytes32,uint8,bytes32,bytes32)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_isValidERC1271SignatureNow(bytes32,bytes)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_isValidSignatureNow(bytes32,bytes)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_tryRecover(address,bytes32,bytes)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/SmolWETH.t.sol:SmolWETHTest": [ + { + "name": "check_deposit_once(address,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/StaticContexts.t.sol:StaticContextsTest": [ + { + "name": "check_create2_fails()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_create_fails()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_log_fails()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_sstore_fails()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Storage.t.sol:StorageTest": [ + { + "name": "check_addArr1(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_addArr2(uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_addMap1Arr1(uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_setMap1(uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_setMap2(uint256,uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_setMap3(uint256,uint256,uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Storage2.t.sol:Storage2Test": [ + { + "name": "check_set((uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint32,uint256,uint32,uint256,uint32,uint256,uint32,uint256,uint32,uint32,uint256,uint32,uint32,uint256,uint32,uint32,uint256,uint32,uint32,uint256,uint256,uint32,uint256,uint256,uint32,uint256,uint256,uint32,uint256,uint256,uint32,uint256,uint32,uint256,uint256,uint32,uint256,uint256,uint32,uint256,uint256,uint32,uint256,uint256,uint32,uint256,uint32,uint256,uint32,uint256,uint32,uint256,uint32,uint256,uint32,uint256,uint32,uint256,uint32,uint256))", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Storage3.t.sol:Storage3Test": [ + { + "name": "check_set((bytes1,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256))", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Storage4.t.sol:Storage4Test": [ + { + "name": "check_add_1(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_add_2(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Store.t.sol:StoreTest": [ + { + "name": "check_store_Array(uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_store_Mapping(uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_store_Scalar(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Struct.t.sol:StructTest": [ + { + "name": "check_Struct((uint256,uint256))", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_StructArray((uint256,uint256)[],(uint256,uint256)[2])", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_StructArrayArray((uint256,uint256)[][],(uint256,uint256)[2][],(uint256,uint256)[][2],(uint256,uint256)[2][2])", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Struct.t.sol:StructTest2": [ + { + "name": "check_S((uint256,uint256[],uint256),(uint256,(uint256,uint256[],uint256),uint256[],(uint256,uint256[],uint256)[],uint256[1],(uint256,uint256[],uint256)[][])[])", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/TestConstructor.t.sol:TestConstructorTest": [ + { + "name": "check_value()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Token.t.sol:TokenTest": [ + { + "name": "check_BalanceInvariant()", + "exitcode": 1, + "num_models": 2, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/UnknownCall.t.sol:UnknownCallTest": [ + { + "name": "check_unknown_call(address,uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_unknown_common_callbacks(address)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_unknown_not_allowed(address)", + "exitcode": 3, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_unknown_retsize_0(address)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_unknown_retsize_64(address)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_unknown_retsize_default(address)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_unknown_send(address,uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_unknown_send_fail(address)", + "exitcode": 4, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_unknown_transfer(address,uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/UnsupportedOpcode.t.sol:X": [ + { + "name": "check_unsupported_opcode()", + "exitcode": 3, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/UnsupportedOpcode.t.sol:Y": [ + { + "name": "check_unsupported_opcode()", + "exitcode": 3, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/UnsupportedOpcode.t.sol:Z": [ + { + "name": "check_unsupported_opcode()", + "exitcode": 3, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Warp.t.sol:WarpTest": [ + { + "name": "check_warp(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_warp_External(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_warp_ExternalSelf(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_warp_Internal(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_warp_New(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_warp_Reset(uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_warp_SetUp()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ] + } +} \ No newline at end of file diff --git a/tests/expected/erc20.json b/tests/expected/erc20.json new file mode 100644 index 00000000..2bd2a5de --- /dev/null +++ b/tests/expected/erc20.json @@ -0,0 +1,150 @@ +{ + "exitcode": 1, + "test_results": { + "test/CurveTokenV3.t.sol:CurveTokenV3Test": [ + { + "name": "check_NoBackdoor(bytes4,address,address)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_transfer(address,address,address,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_transferFrom(address,address,address,address,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/DEIStablecoin.t.sol:DEIStablecoinTest": [ + { + "name": "check_NoBackdoor(bytes4,address,address)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_transfer(address,address,address,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_transferFrom(address,address,address,address,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/OpenZeppelinERC20.t.sol:OpenZeppelinERC20Test": [ + { + "name": "check_NoBackdoor(bytes4,address,address)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_transfer(address,address,address,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_transferFrom(address,address,address,address,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/SoladyERC20.t.sol:SoladyERC20Test": [ + { + "name": "check_NoBackdoor(bytes4,address,address)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_transfer(address,address,address,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_transferFrom(address,address,address,address,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/SolmateERC20.t.sol:SolmateERC20Test": [ + { + "name": "check_NoBackdoor(bytes4,address,address)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_transfer(address,address,address,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_transferFrom(address,address,address,address,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ] + } +} \ No newline at end of file diff --git a/tests/expected/erc721.json b/tests/expected/erc721.json new file mode 100644 index 00000000..e960329b --- /dev/null +++ b/tests/expected/erc721.json @@ -0,0 +1,65 @@ +{ + "exitcode": 0, + "test_results": { + "test/OpenZeppelinERC721.t.sol:OpenZeppelinERC721Test": [ + { + "name": "check_NoBackdoor(bytes4)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_transferFrom(address,address,address,address,uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/SoladyERC721.t.sol:SoladyERC721Test": [ + { + "name": "check_NoBackdoor(bytes4)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_transferFrom(address,address,address,address,uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/SolmateERC721.t.sol:SolmateERC721Test": [ + { + "name": "check_NoBackdoor(bytes4)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_transferFrom(address,address,address,address,uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ] + } +} \ No newline at end of file diff --git a/tests/expected/ffi.json b/tests/expected/ffi.json new file mode 100644 index 00000000..51182e52 --- /dev/null +++ b/tests/expected/ffi.json @@ -0,0 +1,52 @@ +{ + "exitcode": 1, + "test_results": { + "test/Ffi.t.sol:FfiTest": [ + { + "name": "check_Failure()", + "exitcode": 5, + "num_models": null, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_HexOutput()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_ImplicitHexStringOutput()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_Stderr()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_StringOutput()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ] + } +} \ No newline at end of file diff --git a/tests/expected/simple.json b/tests/expected/simple.json new file mode 100644 index 00000000..e1e2e1d7 --- /dev/null +++ b/tests/expected/simple.json @@ -0,0 +1,267 @@ +{ + "exitcode": 1, + "test_results": { + "test/Fork.t.sol:CounterForkTest": [ + { + "name": "check_invariant(address)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_setup()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/IsPowerOfTwo.t.sol:IsPowerOfTwoTest": [ + { + "name": "check_eq_isPowerOfTwo_isPowerOfTwoIter(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_isPowerOfTwo(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_isPowerOfTwo_small(uint8)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Multicaller.t.sol:MulticallerWithSenderSymTest": [ + { + "name": "check_0_0_0_1()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_0_0_0_31()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_0_0_0_32()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_0_0_0_65()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_1_0_0_1()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_1_0_0_31()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_1_0_0_32()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_1_0_0_65()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_1_1_1_1()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_1_1_1_31()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_1_1_1_32()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_1_1_1_65()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_2_2_2_1()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_2_2_2_31()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_2_2_2_32()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_2_2_2_65()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_fallback_0()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_fallback_1()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/TotalPrice.t.sol:TotalPriceTest": [ + { + "name": "check_eq_totalPriceFixed_totalPriceConservative(uint96,uint32)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_totalPriceBuggy(uint96,uint32)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_totalPriceFixed(uint96,uint32)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Vault.t.sol:VaultTest": [ + { + "name": "check_deposit(uint256)", + "exitcode": 2, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_mint(uint256)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ] + } +} \ No newline at end of file diff --git a/tests/expected/solver.json b/tests/expected/solver.json new file mode 100644 index 00000000..9aa51e36 --- /dev/null +++ b/tests/expected/solver.json @@ -0,0 +1,67 @@ +{ + "exitcode": 1, + "test_results": { + "test/Math.t.sol:MathTest": [ + { + "name": "check_Avg(uint256,uint256)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_deposit(uint256,uint256,uint256)", + "exitcode": 2, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_mint(uint256,uint256,uint256)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/SignedDiv.t.sol:TestBadWadMul": [ + { + "name": "check_wadMul_solEquivalent(int256,int256)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/SignedDiv.t.sol:TestGoodWadMul": [ + { + "name": "check_wadMul_solEquivalent(int256,int256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Solver.t.sol:SolverTest": [ + { + "name": "check_foo(uint256,uint256)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ] + } +} \ No newline at end of file diff --git a/tests/ffi/foundry.toml b/tests/ffi/foundry.toml new file mode 100644 index 00000000..3209ea22 --- /dev/null +++ b/tests/ffi/foundry.toml @@ -0,0 +1,11 @@ +[profile.default] +src = 'src' +out = 'out' +libs = ['../lib', 'lib'] + +# See more config options https://github.com/foundry-rs/foundry/tree/master/config +force = false +evm_version = 'shanghai' + +# compile options used by halmos (to prevent unnecessary recompilation when running forge test and halmos together) +extra_output = ["storageLayout", "metadata"] diff --git a/tests/ffi/remappings.txt b/tests/ffi/remappings.txt new file mode 100644 index 00000000..41f9d750 --- /dev/null +++ b/tests/ffi/remappings.txt @@ -0,0 +1,2 @@ +openzeppelin/=../lib/openzeppelin-contracts/contracts/ +ds-test/=../lib/forge-std/lib/ds-test/src/ diff --git a/tests/ffi/test/Ffi.t.sol b/tests/ffi/test/Ffi.t.sol new file mode 100644 index 00000000..ddc85632 --- /dev/null +++ b/tests/ffi/test/Ffi.t.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import "forge-std/Test.sol"; + +contract FfiTest is Test { + + function check_HexOutput() public { + string[] memory inputs = new string[](3); + inputs[0] = "echo"; + inputs[1] = "-n"; + inputs[2] = /* "arbitrary string" abi.encoded hex representation */"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001061726269747261727920737472696e6700000000000000000000000000000000"; + + bytes memory res = vm.ffi(inputs); + + bytes32 expected = keccak256(abi.encodePacked("arbitrary string")); + bytes32 output = keccak256(abi.encodePacked(abi.decode(res, (string)))); + + assert(expected == output); + } + + function check_ImplicitHexStringOutput() public { + string[] memory inputs = new string[](3); + inputs[0] = "echo"; + inputs[1] = "-n"; + inputs[2] = " 4243 "; + + bytes memory res = vm.ffi(inputs); + assertEq(res.length, 2); + assert(res[0] == 0x42); + assert(res[1] == 0x43); + } + + function check_StringOutput() public { + string memory str = "arbitrary string"; + + string[] memory inputs = new string[](3); + inputs[0] = "echo"; + inputs[1] = "-n"; + inputs[2] = str; + + bytes32 expected = keccak256(abi.encodePacked(str)); + bytes32 output = keccak256( + vm.ffi(inputs) /* Perform ffi */ + ); + + assert(expected == output); + } + + function check_Stderr() public { + string[] memory inputs = new string[](3); + inputs[0] = "logger"; + inputs[1] = "-s"; + inputs[2] = "Error!"; + + bytes32 output = keccak256( + vm.ffi(inputs) /* Perform ffi that generates non empty stderr */ + ); + + /* TODO: fix bug in sha3 of empty bytes + bytes32 expected = keccak256(abi.encodePacked("")); + assert(expected == output); + */ + } + + function check_Failure() public { + string[] memory inputs = new string[](1); + inputs[0] = "must_fail"; + + bytes32 output = keccak256( + vm.ffi(inputs) /* Perform ffi that must fail */ + ); + } +} diff --git a/tests/foundry.toml b/tests/foundry.toml deleted file mode 100644 index 0f874219..00000000 --- a/tests/foundry.toml +++ /dev/null @@ -1,8 +0,0 @@ -[profile.default] -src = 'src' -out = 'out' -libs = ['lib'] - -# See more config options https://github.com/foundry-rs/foundry/tree/master/config -force = false -evm_version = 'shanghai' diff --git a/tests/lib/forge-std b/tests/lib/forge-std index 066ff16c..74cfb77e 160000 --- a/tests/lib/forge-std +++ b/tests/lib/forge-std @@ -1 +1 @@ -Subproject commit 066ff16c5c03e6f931cd041fd366bc4be1fae82a +Subproject commit 74cfb77e308dd188d2f58864aaf44963ae6b88b1 diff --git a/tests/lib/halmos-cheatcodes b/tests/lib/halmos-cheatcodes new file mode 160000 index 00000000..c0d86550 --- /dev/null +++ b/tests/lib/halmos-cheatcodes @@ -0,0 +1 @@ +Subproject commit c0d865508c0fee0a11b97732c5e90f9cad6b65a5 diff --git a/tests/lib/multicaller b/tests/lib/multicaller new file mode 160000 index 00000000..b4a0dd03 --- /dev/null +++ b/tests/lib/multicaller @@ -0,0 +1 @@ +Subproject commit b4a0dd037f1d770b2e9ae0b80bbd989707df43d0 diff --git a/tests/lib/openzeppelin-contracts b/tests/lib/openzeppelin-contracts new file mode 160000 index 00000000..21bb89ef --- /dev/null +++ b/tests/lib/openzeppelin-contracts @@ -0,0 +1 @@ +Subproject commit 21bb89ef5bfc789b9333eb05e3ba2b7b284ac77c diff --git a/tests/lib/solady b/tests/lib/solady new file mode 160000 index 00000000..4d4a7572 --- /dev/null +++ b/tests/lib/solady @@ -0,0 +1 @@ +Subproject commit 4d4a7572ad01e84e96370e42bd10d1cc16c1fb65 diff --git a/tests/lib/solmate b/tests/lib/solmate new file mode 160000 index 00000000..bfc9c258 --- /dev/null +++ b/tests/lib/solmate @@ -0,0 +1 @@ +Subproject commit bfc9c25865a274a7827fea5abf6e4fb64fc64e6c diff --git a/tests/regression/foundry.toml b/tests/regression/foundry.toml new file mode 100644 index 00000000..3209ea22 --- /dev/null +++ b/tests/regression/foundry.toml @@ -0,0 +1,11 @@ +[profile.default] +src = 'src' +out = 'out' +libs = ['../lib', 'lib'] + +# See more config options https://github.com/foundry-rs/foundry/tree/master/config +force = false +evm_version = 'shanghai' + +# compile options used by halmos (to prevent unnecessary recompilation when running forge test and halmos together) +extra_output = ["storageLayout", "metadata"] diff --git a/tests/regression/remappings.txt b/tests/regression/remappings.txt new file mode 100644 index 00000000..30f1086e --- /dev/null +++ b/tests/regression/remappings.txt @@ -0,0 +1,3 @@ +openzeppelin/=../lib/openzeppelin-contracts/contracts/ +ds-test/=../lib/forge-std/lib/ds-test/src/ +forge-std/=../lib/forge-std/src/ diff --git a/tests/src/Const.sol b/tests/regression/src/Const.sol similarity index 100% rename from tests/src/Const.sol rename to tests/regression/src/Const.sol diff --git a/tests/src/Counter.sol b/tests/regression/src/Counter.sol similarity index 100% rename from tests/src/Counter.sol rename to tests/regression/src/Counter.sol diff --git a/tests/src/Create.sol b/tests/regression/src/Create.sol similarity index 100% rename from tests/src/Create.sol rename to tests/regression/src/Create.sol diff --git a/tests/src/List.sol b/tests/regression/src/List.sol similarity index 100% rename from tests/src/List.sol rename to tests/regression/src/List.sol diff --git a/tests/src/SignExtend.sol b/tests/regression/src/SignExtend.sol similarity index 100% rename from tests/src/SignExtend.sol rename to tests/regression/src/SignExtend.sol diff --git a/tests/src/Storage.sol b/tests/regression/src/Storage.sol similarity index 100% rename from tests/src/Storage.sol rename to tests/regression/src/Storage.sol diff --git a/tests/test/Arith.t.sol b/tests/regression/test/Arith.t.sol similarity index 81% rename from tests/test/Arith.t.sol rename to tests/regression/test/Arith.t.sol index a17ca944..eb5b4225 100644 --- a/tests/test/Arith.t.sol +++ b/tests/regression/test/Arith.t.sol @@ -9,7 +9,7 @@ contract ArithTest { } } - function testMod(uint x, uint y, address addr) public pure { + function check_Mod(uint x, uint y, address addr) public pure { unchecked { assert(unchecked_mod(x, 0) == 0); // compiler rejects `x % 0` assert(x % 1 == 0); @@ -17,14 +17,14 @@ contract ArithTest { assert(x % 4 < 4); uint x_mod_y = unchecked_mod(x, y); - // assert(x_mod_y == 0 || x_mod_y < y); + // assert(x_mod_y == 0 || x_mod_y < y); // not supported // TODO: support more axioms assert(x_mod_y <= y); assert(uint256(uint160(addr)) % (2**160) == uint256(uint160(addr))); } } - function testExp(uint x) public pure { + function check_Exp(uint x) public pure { unchecked { assert(x ** 0 == 1); // 0 ** 0 == 1 assert(x ** 1 == x); diff --git a/tests/regression/test/AssertTest.t.sol b/tests/regression/test/AssertTest.t.sol new file mode 100644 index 00000000..c63386c9 --- /dev/null +++ b/tests/regression/test/AssertTest.t.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import "forge-std/Test.sol"; + +contract C is Test { + function foo() public pure { + assert(false); // not propagated + } + + function bar() public { + fail(); // propagated + } +} + +contract AssertTest is Test { + C c; + + function setUp() public { + c = new C(); + } + + function check_assert_not_propagated() public { + address(c).call(abi.encodeWithSelector(C.foo.selector, bytes(""))); // pass + } + + function check_fail_propagated() public { + address(c).call(abi.encodeWithSelector(C.bar.selector, bytes(""))); // fail + } +} diff --git a/tests/test/Block.t.sol b/tests/regression/test/Block.t.sol similarity index 67% rename from tests/test/Block.t.sol rename to tests/regression/test/Block.t.sol index 2f842c95..ebb7097d 100644 --- a/tests/test/Block.t.sol +++ b/tests/regression/test/Block.t.sol @@ -4,32 +4,32 @@ pragma solidity >=0.8.0 <0.9.0; import "forge-std/Test.sol"; contract BlockCheatCodeTest is Test { - function testFee(uint x) public { + function check_fee(uint x) public { vm.fee(x); assert(block.basefee == x); } - function testChainId(uint64 x) public { + function check_chainId(uint64 x) public { vm.chainId(x); assert(block.chainid == x); } - function testCoinbase(address x) public { + function check_coinbase(address x) public { vm.coinbase(x); assert(block.coinbase == x); } - function testDifficulty(uint x) public { + function check_difficulty(uint x) public { vm.difficulty(x); assert(block.difficulty == x); } - function testRoll(uint x) public { + function check_roll(uint x) public { vm.roll(x); assert(block.number == x); } - function testWarp(uint x) public { + function check_warp(uint x) public { vm.warp(x); assert(block.timestamp == x); } diff --git a/tests/regression/test/Buffers.t.sol b/tests/regression/test/Buffers.t.sol new file mode 100644 index 00000000..f377dbe9 --- /dev/null +++ b/tests/regression/test/Buffers.t.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; +import "forge-std/Test.sol"; + +contract BuffersTest is Test { + function check_calldatacopy_large_offset() public { + uint256 index = 1 ether; + uint256 value; + assembly { + calldatacopy(0, index, 32) + value := mload(0) + } + + assertEq(value, 0); + } + + function check_calldataload_large_offset() public { + uint256 index = 1 ether; + uint256 value; + assembly { + value := calldataload(index) + } + + assertEq(value, 0); + } + + function check_codecopy_large_offset() public { + uint256 index = 1 ether; + uint256 value; + assembly { + codecopy(0, index, 32) + value := mload(0) + } + + assertEq(value, 0); + } + + function check_codecopy_offset_across_boundary() public { + uint256 index = address(this).code.length - 16; + uint256 value; + assembly { + codecopy(0, index, 32) + value := mload(0) + } + + assertNotEq(value, 0); + } + + // TODO: uncomment when we support extcodecopy + // function check_extcodecopy_boundary() public { + // address target = address(this); + // uint256 index = target.code.length - 16; + // uint256 value; + // assembly { + // extcodecopy(target, 0, index, 32) + // value := mload(0) + // } + + // console2.log("value:", value); + // assertNotEq(value, 0); + // } +} diff --git a/tests/regression/test/Byte.t.sol b/tests/regression/test/Byte.t.sol new file mode 100644 index 00000000..d10b0b4b --- /dev/null +++ b/tests/regression/test/Byte.t.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +contract ByteTest { + function byte1(uint i, uint x) public pure returns (uint r) { + assembly { r := byte(i, x) } + } + + function byte2(uint i, uint x) public pure returns (uint) { + if (i >= 32) return 0; + return (x >> (248-i*8)) & 0xff; + } + + function byte3(uint i, uint x) public pure returns (uint) { + if (i >= 32) return 0; + bytes memory b = new bytes(32); + assembly { mstore(add(b, 32), x) } + return uint(uint8(bytes1(b[i]))); // TODO: Not supported: MLOAD symbolic memory offset: 160 + p_i_uint256 + } + + function check_byte(uint i, uint x) pure public { + uint r1 = byte1(i, x); + uint r2 = byte2(i, x); + // uint r3 = byte3(i, x); // not supported + assert(r1 == r2); + // assert(r1 == r3); + } +} + +contract SymbolicByteTest { + function check_SymbolicByteIndex(uint8 x, uint8 i) public pure returns (uint r) { + if (x > 10) assert(false); // expected to fail + assembly { + r := byte(i, x) + } + assert(r == 0); // expected to fail with counterexample + } +} diff --git a/tests/regression/test/Call.t.sol b/tests/regression/test/Call.t.sol new file mode 100644 index 00000000..229f6015 --- /dev/null +++ b/tests/regression/test/Call.t.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import "forge-std/Test.sol"; + +contract C { + uint public num; + function foo(uint x) public payable returns (address, uint, address) { + if (x > 0) num = x; + return (msg.sender, msg.value, address(this)); + } +} + +contract D { + uint public num; + C public c; + + constructor () { + c = new C(); + } + + function call_foo(uint x) public payable returns (bool success, bytes memory retdata) { + (success, retdata) = address(c).call{ value: msg.value }(abi.encodeWithSelector(c.foo.selector, x)); + } + + function staticcall_foo(uint x) public payable returns (bool success, bytes memory retdata) { + (success, retdata) = address(c).staticcall(abi.encodeWithSelector(c.foo.selector, x)); + } + + function delegatecall_foo(uint x) public payable returns (bool success, bytes memory retdata) { + (success, retdata) = address(c).delegatecall(abi.encodeWithSelector(c.foo.selector, x)); + } + + function callcode_foo(uint x) public payable returns (bool success, bytes memory retdata) { + address msg_sender; + uint msg_value; + address target; + + bytes4 sig = c.foo.selector; + + assembly { + let m := mload(0x40) + mstore(m, sig) + mstore(add(m, 0x04), x) + success := callcode(gas(), sload(c.slot), callvalue(), m, 0x24, m, 0x60) + msg_sender := mload(m) + msg_value := mload(add(m, 0x20)) + target := mload(add(m, 0x40)) + } + + retdata = abi.encode(msg_sender, msg_value, target); + } +} + +contract CallTest is Test { + D d; + + function setUp() public { + d = new D(); + } + + function check_call(uint x, uint fund) public payable { + vm.deal(address(this), fund); + vm.deal(address(d), 0); + vm.deal(address(d.c()), 0); + + (bool success, bytes memory retdata) = d.call_foo{ value: fund }(x); + vm.assume(success); + (address msg_sender, uint msg_value, address target) = abi.decode(retdata, (address, uint, address)); + + assert(msg_sender == address(d)); + assert(msg_value == fund); + assert(target == address(d.c())); + + assert(d.num() == 0); + assert(d.c().num() == x); + + assert(address(this).balance == 0); + assert(address(d).balance == 0); + assert(address(d.c()).balance == fund); + } + + function check_staticcall(uint x, uint fund) public payable { + vm.deal(address(this), fund); + vm.deal(address(d), 0); + vm.deal(address(d.c()), 0); + + (bool success, bytes memory retdata) = d.staticcall_foo{ value: fund }(x); + vm.assume(success); + (address msg_sender, uint msg_value, address target) = abi.decode(retdata, (address, uint, address)); + + assert(msg_sender == address(d)); + assert(msg_value == 0); // no fund transfer for staticcall + assert(target == address(d.c())); + + assert(d.num() == 0); + assert(d.c().num() == x); + + assert(address(this).balance == 0); + assert(address(d).balance == fund); + assert(address(d.c()).balance == 0); + } + + function check_delegatecall(uint x, uint fund) public payable { + vm.deal(address(this), fund); + vm.deal(address(d), 0); + vm.deal(address(d.c()), 0); + + (bool success, bytes memory retdata) = d.delegatecall_foo{ value: fund }(x); + vm.assume(success); + (address msg_sender, uint msg_value, address target) = abi.decode(retdata, (address, uint, address)); + + // delegatecall is executed in the caller's context + assertEq(msg_sender, address(this)); + assertEq(msg_value, fund); + assertEq(target, address(d)); + + assert(d.num() == x); // delegatecall updates the caller's state + assert(d.c().num() == 0); + + assert(address(this).balance == 0); + assert(address(d).balance == fund); // no fund transfer for delegatecall + assert(address(d.c()).balance == 0); + } + + function check_callcode(uint x, uint fund) public payable { + vm.deal(address(this), fund); + vm.deal(address(d), 0); + vm.deal(address(d.c()), 0); + + (bool success, bytes memory retdata) = d.callcode_foo{ value: fund }(x); + vm.assume(success); + (address msg_sender, uint msg_value, address target) = abi.decode(retdata, (address, uint, address)); + + assert(msg_sender == address(d)); + assert(msg_value == fund); + assert(target == address(d)); // callcode calls to itself + + assert(d.num() == x); // callcode updates the caller's state + assert(d.c().num() == 0); + + assert(address(this).balance == 0); + assert(address(d).balance == fund); // fund is transfered to itself + assert(address(d.c()).balance == 0); + } +} diff --git a/tests/regression/test/CallAlias.t.sol b/tests/regression/test/CallAlias.t.sol new file mode 100644 index 00000000..99077a48 --- /dev/null +++ b/tests/regression/test/CallAlias.t.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import "forge-std/Test.sol"; + +contract C { + uint public num; + + function set(uint x) public { + num = x; + } +} + +contract CallAliasTest is Test { + C c1; + C c2; + + function setUp() public { + c1 = new C(); + c2 = new C(); + } + + function check_alias_1(address addr, uint x) public { + if (addr == address(c1)) { + C(addr).set(x); + assert(c1.num() == x); + assert(c2.num() == 0); + } else if (addr == address(c2)) { + C(addr).set(x); + assert(c1.num() == 0); + assert(c2.num() == x); + } + } + + function check_alias_2(address addr, uint x) public { + if (addr == address(c1)) { + assert(addr.codehash == address(c1).codehash); + assert(addr.code.length == address(c1).code.length); + assert(addr.code.length > 0); + } else if (addr == address(this)) { + assert(addr.codehash == address(this).codehash); + assert(addr.code.length == address(this).code.length); + assert(addr.code.length > 0); + } + } + + function check_alias_3(address addr, uint x) public { + if (addr == address(c1)) { + vm.store(addr, bytes32(0), bytes32(x)); + assert(c1.num() == x); + assert(c2.num() == 0); + assert(uint(vm.load(addr, bytes32(0))) == x); + } else if (addr == address(c2)) { + vm.store(addr, bytes32(0), bytes32(x)); + assert(c1.num() == 0); + assert(c2.num() == x); + assert(uint(vm.load(addr, bytes32(0))) == x); + } + } + + function check_alias_1a(bool mode, address addr, uint x) public { + if (mode) { + vm.assume(addr == address(c1)); + } else { + vm.assume(addr == address(c2)); + } + + C(addr).set(x); + + if (mode) { + assert(c1.num() == x); + assert(c2.num() == 0); + } else { + assert(c1.num() == 0); + assert(c2.num() == x); + } + } + + function check_alias_2a(bool mode, address addr, uint x) public { + if (mode) { + vm.assume(addr == address(c1)); + } else { + vm.assume(addr == address(this)); + } + + if (mode) { + assert(addr.codehash == address(c1).codehash); + assert(addr.code.length == address(c1).code.length); + } else { + assert(addr.codehash == address(this).codehash); + assert(addr.code.length == address(this).code.length); + } + + assert(addr.code.length > 0); + } + + function check_alias_3a(bool mode, address addr, uint x) public { + if (mode) { + vm.assume(addr == address(c1)); + } else { + vm.assume(addr == address(c2)); + } + + vm.store(addr, bytes32(0), bytes32(x)); + + if (mode) { + assert(c1.num() == x); + assert(c2.num() == 0); + } else { + assert(c1.num() == 0); + assert(c2.num() == x); + } + + assert(uint(vm.load(addr, bytes32(0))) == x); + } +} diff --git a/tests/regression/test/Console.t.sol b/tests/regression/test/Console.t.sol new file mode 100644 index 00000000..35002b78 --- /dev/null +++ b/tests/regression/test/Console.t.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import "forge-std/Test.sol"; +import {console2} from "forge-std/console2.sol"; + +contract ConsoleTest is Test { + function check_log_uint() public view { + console2.log("this is 0:", uint256(0)); + console2.log("this is 1:", uint256(1)); + + console2.log(uint256(0)); + console2.log(uint256(1)); + + console2.logUint(0); + console2.logUint(1); + } + + function check_log_int() public view { + console2.log("this is -1:", -1); + console2.log("this is 1:", int256(1)); + + console2.log(-1); + console2.log(int256(1)); + + console2.logInt(-1); + console2.logInt(int256(1)); + } + + function check_log_bytes() public view { + bytes memory hello = "hello"; + bytes memory empty = ""; + console2.log("this is hello (bytes):"); + console2.logBytes(hello); + console2.log("this is empty bytes:"); + console2.logBytes(empty); + } + + function check_log_bytes32() public view { + console2.log("this is keccak256(hello):"); + console2.logBytes32(keccak256("hello")); + + console2.log("this is keccak256():"); + console2.logBytes32(keccak256("")); + } + + function check_log_address() public view { + console2.log("this is address(0):", address(0)); + console2.log("this is address(this):", address(this)); + + console2.log(address(0)); + console2.log(address(this)); + } + + function check_log_bool() public view { + console2.log("this is true:", true); + console2.log("this is false:", false); + + console2.log(true); + console2.log(false); + } + + function check_log_string() public view { + string memory hello = "hello"; + string memory empty = ""; + console2.log("this is hello (string):", hello); + console2.log("this is empty string:", empty); + + console2.log(hello); + console2.log(empty); + } + + function check_log_undecodable_string() public view { + bytes memory badBytes = hex"ff"; + string memory bad = string(badBytes); + console2.log("this is a string that won't decode to utf-8:", bad); + } + + function check_log_unsupported() public { + console2._sendLogPayload(abi.encodeWithSignature("doesNotExist()")); + } +} diff --git a/tests/test/Const.t.sol b/tests/regression/test/Const.t.sol similarity index 77% rename from tests/test/Const.t.sol rename to tests/regression/test/Const.t.sol index 712206e9..9f03fab8 100644 --- a/tests/test/Const.t.sol +++ b/tests/regression/test/Const.t.sol @@ -5,13 +5,13 @@ import "forge-std/Test.sol"; import "../src/Const.sol"; contract ConstTest is Const { - function testConst() public pure { + function check_Const() public pure { assert(const == 11); } } contract ConstTestTest is Const, Test { - function testConst() public { + function check_Const() public { assertEq(const, 11); } } diff --git a/tests/regression/test/Constructor.t.sol b/tests/regression/test/Constructor.t.sol new file mode 100644 index 00000000..0bf9069a --- /dev/null +++ b/tests/regression/test/Constructor.t.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +contract C { + uint public codesize_; + uint public extcodesize_; + + constructor () { + setCodesize(); + } + + function setCodesize() public { + assembly { + sstore(codesize_.slot, codesize()) + sstore(extcodesize_.slot, extcodesize(address())) + } + } +} + +contract ConstructorTest { + C c; + + function setUp() public { + c = new C(); + } + + function check_constructor() public { + assert(c.codesize_() > 0); // contract creation bytecode size + assert(c.extcodesize_() == 0); // code is not yet deposited in the network state during constructor() + } + + function check_setCodesize() public { + uint creation_codesize = c.codesize_(); + + c.setCodesize(); + + assert(c.codesize_() > 0); // deployed bytecode size + assert(c.extcodesize_() > 0); // deployed bytecode size + assert(c.codesize_() == c.extcodesize_()); + + assert(c.codesize_() < creation_codesize); // deployed bytecode is smaller than creation bytecode + } +} diff --git a/tests/regression/test/Context.t.sol b/tests/regression/test/Context.t.sol new file mode 100644 index 00000000..0733ecc5 --- /dev/null +++ b/tests/regression/test/Context.t.sol @@ -0,0 +1,281 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import "forge-std/Test.sol"; + +contract Context is Test { + bool public flag; + + function call0(uint mode) external { + flag = true; + + if (mode == 1) { + assembly { + stop() + } + } else if (mode == 2) { + assembly { + invalid() + } + } else if (mode == 3) { + assembly { + revert(0, 0) + } + } else if (mode == 4) { + revert("blah"); // revert(Error("blah")) + } else if (mode == 5) { + assert(false); // revert(Panic(1)) + } else if (mode == 6) { + assembly { + return(0, 0) + } + } else if (mode == 7) { + assembly { + return(0, 32) + } + } else if (mode == 8) { + assembly { + let p := mload(0x40) + returndatacopy(p, returndatasize(), 32) // OutOfBoundsRead + } + } else if (mode == 9) { + vm.prank(address(0)); + vm.prank(address(0)); // HalmosException + } else if (mode == 10) { + fail(); + } + } + + function call1(uint mode1, address ctx0, uint mode0) external returns (bool success, bytes memory retdata) { + flag = true; + + (success, retdata) = ctx0.call(abi.encodeWithSelector(Context.call0.selector, mode0)); + + if (mode1 == 0) { + bytes memory result = abi.encode(success, retdata); + assembly { + revert(add(32, result), mload(result)) + } + } + } +} + +contract ConstructorContext { + constructor(Context ctx, uint mode, bool fail) { + assert(returndatasize() == 0); // empty initial returndata + assert(msg.data.length == 0); // no calldata for constructor + + ctx.call0(mode); + + if (fail) revert("fail"); + } + + function returndatasize() internal pure returns (uint size) { + assembly { + size := returndatasize() + } + } +} + +contract ContextTest is Test { + address internal testDeployer; + address internal testAddress; + + Context internal ctx; + + constructor() payable { + assertEq(returndatasize(), 0); // empty initial returndata + assertEq(msg.data.length, 0); // no calldata for test constructor + assertEq(msg.value, 0); // no callvalue + testDeployer = msg.sender; + testAddress = address(this); + } + + function ensure_test_context() internal { + assertEq(address(this), testAddress); + assertEq(msg.sender, testDeployer); + assertEq(msg.value, 0); // no callvalue + assertGt(msg.data.length, 0); // non-empty calldata + } + + function setUp() public payable { + assertEq(returndatasize(), 0); // empty initial returndata + ensure_test_context(); + + ctx = new Context(); + + assertEq(returndatasize(), 0); // empty returndata after create + } + + function check_setup() public payable { + assert(returndatasize() == 0); // empty initial returndata + ensure_test_context(); + } + + function check_returndata() public payable { + assert(returndatasize() == 0); // empty initial returndata + ensure_test_context(); + + address(ctx).call(abi.encodeWithSelector(Context.call0.selector, 1)); + assert(returndatasize() == 0); + ensure_test_context(); + + address(ctx).call(abi.encodeWithSelector(Context.call0.selector, 2)); + assert(returndatasize() == 0); + ensure_test_context(); + + address(ctx).call(abi.encodeWithSelector(Context.call0.selector, 3)); + assert(returndatasize() == 0); + ensure_test_context(); + + address(ctx).call(abi.encodeWithSelector(Context.call0.selector, 4)); + assert(returndatasize() == 100); + ensure_test_context(); + + address(ctx).call(abi.encodeWithSelector(Context.call0.selector, 5)); + assert(returndatasize() == 36); + ensure_test_context(); + + address(ctx).call(abi.encodeWithSelector(Context.call0.selector, 6)); + assert(returndatasize() == 0); + ensure_test_context(); + + address(ctx).call(abi.encodeWithSelector(Context.call0.selector, 7)); + assert(returndatasize() == 32); + ensure_test_context(); + + address(ctx).call(abi.encodeWithSelector(Context.call0.selector, 8)); + assert(returndatasize() == 0); + ensure_test_context(); + + new Context(); + assert(returndatasize() == 0); // empty returndata after create + ensure_test_context(); + } + + function check_create() public payable { + assertEq(returndatasize(), 0); // empty initial returndata + ensure_test_context(); + + Context ctx1 = new Context(); + ConstructorContext cc1 = new ConstructorContext(ctx1, 7, false); + cc1; // silence unused warning + assertEq(returndatasize(), 0); // empty returndata after create + ensure_test_context(); + + assertTrue(ctx1.flag()); + assertEq(returndatasize(), 32); + ensure_test_context(); + + Context ctx2 = new Context(); + try new ConstructorContext(ctx2, 7, true) returns (ConstructorContext cc2) { + cc2; // silence unused warning + } catch {} + assertEq(returndatasize(), 100); // returndata for create failure + ensure_test_context(); + + assertFalse(ctx2.flag()); + assertEq(returndatasize(), 32); + ensure_test_context(); + } + + function check_create_halmos_exception() public payable { + assert(returndatasize() == 0); // empty initial returndata + ensure_test_context(); + + try new ConstructorContext(ctx, 9, false) returns (ConstructorContext cc) { + cc; // silence unused warning + } catch {} + + assert(false); // shouldn't reach here + } + + function check_call0_normal(uint mode0) public payable { + require(mode0 < 9); + _check_call0(mode0); + } + + function check_call0_halmos_exception() public payable { + _check_call0(9); + } + + function check_call0_fail() public payable { + _check_call0(10); + } + + function _check_call0(uint mode0) public payable { + assert(returndatasize() == 0); // empty initial returndata + ensure_test_context(); + + Context ctx0 = ctx; + + (bool success0, bytes memory retdata0) = address(ctx0).call(abi.encodeWithSelector(Context.call0.selector, mode0)); + assert(returndatasize() == retdata0.length); + ensure_test_context(); + + ensure_call0_result(mode0, success0, retdata0); + + assert(ctx0.flag() == success0); // revert state + assert(returndatasize() == 32); + ensure_test_context(); + } + + function check_call1_normal(uint mode1, uint mode0) public payable { + require(mode0 < 9); + _check_call1(mode1, mode0); + } + + function check_call1_halmos_exception(uint mode1) public payable { + _check_call1(mode1, 9); + } + + function check_call1_fail(uint mode1) public payable { + _check_call1(mode1, 10); + } + + function _check_call1(uint mode1, uint mode0) public payable { + assert(returndatasize() == 0); // empty initial returndata + ensure_test_context(); + + Context ctx1 = new Context(); + Context ctx0 = ctx; + + (bool success1, bytes memory retdata1) = address(ctx1).call(abi.encodeWithSelector(Context.call1.selector, mode1, ctx0, mode0)); + assert(returndatasize() == retdata1.length); + ensure_test_context(); + + assert(success1 == (mode1 > 0)); + (bool success0, bytes memory retdata0) = abi.decode(retdata1, (bool, bytes)); + ensure_test_context(); + + ensure_call0_result(mode0, success0, retdata0); + + // revert state + assert(ctx1.flag() == success1); + assert(returndatasize() == 32); + ensure_test_context(); + + assert(ctx0.flag() == (success1 && success0)); + assert(returndatasize() == 32); + ensure_test_context(); + } + + function ensure_call0_result(uint mode, bool success, bytes memory retdata) internal { + if (mode == 1) assert( success && retdata.length == 0); + else if (mode == 2) assert(!success && retdata.length == 0); + else if (mode == 3) assert(!success && retdata.length == 0); + else if (mode == 4) assert(!success && retdata.length > 0); + else if (mode == 5) assert(!success && retdata.length == 36); + else if (mode == 6) assert( success && retdata.length == 0); + else if (mode == 7) assert( success && retdata.length == 32); + else if (mode == 8) assert(!success && retdata.length == 0); + else if (mode == 9) assert(!success && retdata.length == 0); + else if (mode == 10) assert(!success && retdata.length == 0); + } + + function returndatasize() internal pure returns (uint size) { + assembly { + size := returndatasize() + } + } +} diff --git a/tests/test/Counter.t.sol b/tests/regression/test/Counter.t.sol similarity index 72% rename from tests/test/Counter.t.sol rename to tests/regression/test/Counter.t.sol index beddeb0c..02960bce 100644 --- a/tests/test/Counter.t.sol +++ b/tests/regression/test/Counter.t.sol @@ -3,20 +3,21 @@ pragma solidity >=0.8.0 <0.9.0; import "../src/Counter.sol"; +/// @custom:halmos --loop 4 --symbolic-storage contract CounterTest is Counter { - function testSet(uint n) public { + function check_set(uint n) public { set(n); assert(cnt == n); } - function testInc() public { + function check_inc() public { uint oldCnt = cnt; inc(); assert(cnt > oldCnt); assert(cnt == oldCnt + 1); } - function testIncOpt() public { + function check_incOpt() public { uint oldCnt = cnt; require(cnt < type(uint).max); incOpt(); @@ -24,7 +25,7 @@ contract CounterTest is Counter { assert(cnt == oldCnt + 1); } - function testIncBy(uint n) public { + function check_incBy(uint n) public { uint oldCnt = cnt; incBy(n); assert(cnt < oldCnt || cnt == oldCnt + n); // cnt >= oldCnt ==> cnt == oldCnt + n @@ -36,7 +37,7 @@ contract CounterTest is Counter { assert(cnt >= oldCnt); assert(cnt == oldCnt + n); } - function testLoopFor(uint8 k) public { + function check_loopFor(uint8 k) public { specLoopFor(k); } @@ -46,7 +47,7 @@ contract CounterTest is Counter { assert(cnt >= oldCnt); assert(cnt == oldCnt + n); } - function testLoopWhile(uint8 k) public { + function check_loopWhile(uint8 k) public { specLoopWhile(k); } @@ -57,18 +58,18 @@ contract CounterTest is Counter { if (n == 0) assert(cnt == oldCnt + 1); else assert(cnt == oldCnt + n); } - function testLoopDoWhile(uint8 k) public { + function check_loopDoWhile(uint8 k) public { specLoopDoWhile(k); } - function testLoopConst() public { + function check_loopConst() public { uint oldCnt = cnt; loopConst(); assert(cnt >= oldCnt); assert(cnt == oldCnt + 2); } - function testLoopConstIf() public { + function check_loopConstIf() public { uint oldCnt = cnt; loopConstIf(); assert(cnt >= oldCnt); @@ -79,36 +80,36 @@ contract CounterTest is Counter { setSum(arr); assert(cnt == arr[0] + arr[1]); } - function testSetSum(uint248 a, uint248 b) public { + function check_setSum(uint248 a, uint248 b) public { specSetSum([uint(a), b]); } - function testSetString(uint, string memory s, uint, string memory r, uint) public { + function check_setString(uint, string memory s, uint, string memory r, uint) public { uint oldCnt = cnt; setString(s); setString(r); assert(cnt == oldCnt + bytes(s).length + bytes(r).length); } - function testFoo(uint a, uint b, uint c, uint d) public { + function check_foo(uint a, uint b, uint c, uint d) public { uint oldCnt = cnt; foo(a, b, c, d); assert(cnt == oldCnt + 4); } - function testDiv1(uint x, uint y) public pure { + function check_div_1(uint x, uint y) public pure { if (y > 0) { assert(x / y <= x); } } - function testDiv2(uint x, uint y) public pure { + function check_div_2(uint x, uint y) public pure { if (y > 0) { assert(x / y == x / y); } } - function testMulDiv(uint x, uint y) public pure { + function check_mulDiv(uint x, uint y) public pure { unchecked { if (x > 0 && y > 0) { uint z = x * y; @@ -120,8 +121,8 @@ contract CounterTest is Counter { } } - /* TODO: support testFail prefix - function testFail() public pure { + /* TODO: support checkFail prefix + function checkFail() public pure { require(false); // deadcode } diff --git a/tests/test/Create.t.sol b/tests/regression/test/Create.t.sol similarity index 68% rename from tests/test/Create.t.sol rename to tests/regression/test/Create.t.sol index dd019630..4c32e305 100644 --- a/tests/test/Create.t.sol +++ b/tests/regression/test/Create.t.sol @@ -11,26 +11,26 @@ contract CreateTest is Test { create = new Create(0x220E); } - /* TODO: support testFail prefix - function testFailSetUp() public { + /* TODO: support checkFail prefix + function checkFail_setUp() public { assertEq(create.value(), 0); } */ - function testSet(uint x) public { + function check_set(uint x) public { create.set(x); assertEq(create.value(), x); } - function testImmutable() public { + function check_immutable() public { assertEq(create.halmos(), 0x220E); } - function testInitialized() public { + function check_initialized() public { assertEq(create.initialized(), 7); } - function testConst() public { + function check_const() public { assertEq(create.const(), 11); } } diff --git a/tests/regression/test/Create2.t.sol b/tests/regression/test/Create2.t.sol new file mode 100644 index 00000000..2dc27586 --- /dev/null +++ b/tests/regression/test/Create2.t.sol @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import "forge-std/Test.sol"; + +contract C { + uint public num1; + uint public num2; + + constructor(uint x, uint y) { + set(x, y); + } + + function set(uint x, uint y) public { + num1 = x; + num2 = y; + } +} + +contract Create2Test is Test { + function check_create2(uint x, uint y, bytes32 salt) public { + C c1 = new C{salt: salt}(x, y); + + bytes32 codeHash = keccak256(abi.encodePacked(type(C).creationCode, abi.encode(x, y))); + bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, codeHash)); + address c2 = address(uint160(uint(hash))); + + assert(address(c1) == c2); + + assert(C(c2).num1() == x); + assert(C(c2).num2() == y); + + c1.set(y, x); + + assert(C(c2).num1() == y); + assert(C(c2).num2() == x); + } + + function check_create2_caller(address caller, uint x, uint y, bytes32 salt) public { + vm.prank(caller); + C c1 = new C{salt: salt}(x, y); + + bytes32 codeHash = keccak256(abi.encodePacked(type(C).creationCode, abi.encode(x, y))); + bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), caller, salt, codeHash)); + address c2 = address(uint160(uint(hash))); + + assert(address(c1) == c2); + + assert(C(c2).num1() == x); + assert(C(c2).num2() == y); + + c1.set(y, x); + + assert(C(c2).num1() == y); + assert(C(c2).num2() == x); + } + + function check_create2_concrete() public { + uint x = 1; + uint y = 2; + bytes32 salt = bytes32(uint(3)); + + C c1 = new C{salt: salt}(x, y); + + bytes32 codeHash = keccak256(abi.encodePacked(type(C).creationCode, abi.encode(x, y))); + bytes32 hash = keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, codeHash)); + address c2 = address(uint160(uint(hash))); + + assert(address(c1) == c2); + + assert(C(c2).num1() == x); + assert(C(c2).num2() == y); + + c1.set(y, x); + + assert(C(c2).num1() == y); + assert(C(c2).num2() == x); + } + + function check_create2_collision_basic(uint x, uint y, bytes32 salt) public { + C c1 = new C{salt: salt}(x, y); + C c2 = new C{salt: salt}(x, y); // expected to fail (Solidity reverts) + c1; c2; // silence warnings + } + + function check_create2_collision_lowlevel(uint x, uint y, bytes32 salt) public { + bytes memory deploymentBytecode = abi.encodePacked(type(C).creationCode, abi.encode(x, y)); + + address addr1; + address addr2; + + assembly { + addr1 := create2( + 0, // value + add(deploymentBytecode, 32), // data offset + mload(deploymentBytecode), // data length + salt + ) + + addr2 := create2( + 0, // value + add(deploymentBytecode, 32), // data offset + mload(deploymentBytecode), // data length + salt + ) + } + + assert(addr1 != address(0)); + assert(addr2 == address(0)); // expected to fail without reverting + } + + function check_create2_no_collision_1(uint x, uint y, bytes32 salt1, bytes32 salt2) public { + C c1 = new C{salt: salt1}(x, y); + C c2 = new C{salt: salt2}(x, y); + assert(c1 != c2); + } + + function check_create2_no_collision_2(uint x, uint y, bytes32 salt) public { + vm.assume(x != y); + + C c1 = new C{salt: salt}(x, y); + C c2 = new C{salt: salt}(y, x); + assert(c1 != c2); + } + + function check_create2_collision_alias(uint x, uint y, bytes32 salt) public { + vm.assume(x == y); + + C c1 = new C{salt: salt}(x, y); + C c2 = new C{salt: salt}(y, x); + assert(c1 == c2); // currently fail // TODO: support symbolic alias for hash + } +} diff --git a/tests/test/Deal.t.sol b/tests/regression/test/Deal.t.sol similarity index 76% rename from tests/test/Deal.t.sol rename to tests/regression/test/Deal.t.sol index 49470e1b..d26e4066 100644 --- a/tests/test/Deal.t.sol +++ b/tests/regression/test/Deal.t.sol @@ -6,18 +6,18 @@ import "forge-std/Test.sol"; contract DealTest is Test { C c; - function testDeal1(address payable receiver, uint amount) public { + function check_deal_1(address payable receiver, uint amount) public { vm.deal(receiver, amount); assert(receiver.balance == amount); } - function testDeal2(address payable receiver, uint amount1, uint amount2) public { + function check_deal_2(address payable receiver, uint amount1, uint amount2) public { vm.deal(receiver, amount1); vm.deal(receiver, amount2); // reset the balance, not increasing assert(receiver.balance == amount2); } - function testDealNew() public { + function check_deal_new() public { vm.deal(address(this), 3 ether); c = new C{value: 3 ether}(); diff --git a/tests/test/Foundry.t.sol b/tests/regression/test/Foundry.t.sol similarity index 67% rename from tests/test/Foundry.t.sol rename to tests/regression/test/Foundry.t.sol index 955dd184..556d4d3d 100644 --- a/tests/test/Foundry.t.sol +++ b/tests/regression/test/Foundry.t.sol @@ -6,19 +6,34 @@ import "forge-std/StdCheats.sol"; import "../src/Counter.sol"; +contract DeepFailer is Test { + function do_test(uint256 x) external { + if (x >= 6) { + fail(); + } else { + (bool success, ) = address(this).call(abi.encodeWithSelector(this.do_test.selector, x + 1)); + success; // silence warnings + } + } + + function test_fail_cheatcode() public { + DeepFailer(address(this)).do_test(0); + } +} + contract FoundryTest is Test { - /* TODO: support testFail prefix - function testFail() public { + /* TODO: support checkFail prefix + function checkFail() public { assertTrue(false); } */ - function testAssume(uint x) public { + function check_assume(uint x) public { vm.assume(x < 10); assertLt(x, 100); } - function testGetCode(uint x) public { + function check_deployCode(uint x) public { Counter counter = Counter(deployCode("./out/Counter.sol/Counter.json")); counter.set(x); assertEq(counter.cnt(), x); @@ -28,7 +43,7 @@ contract FoundryTest is Test { assertEq(counter2.cnt(), x); } - function testEtchConcrete() public { + function check_etch_Concrete() public { vm.etch(address(0x42), hex"60425f526001601ff3"); (bool success, bytes memory retval) = address(0x42).call(""); @@ -37,7 +52,7 @@ contract FoundryTest is Test { assertEq(uint256(uint8(retval[0])), 0x42); } - function testEtchOverwrite() public { + function check_etch_Overwrite() public { vm.etch(address(0x42), hex"60425f526001601ff3"); (, bytes memory retval) = address(0x42).call(""); @@ -51,8 +66,10 @@ contract FoundryTest is Test { assertEq(uint256(uint8(retval[0])), 0xAA); } + + /// @notice etching to a symbolic address is not supported - // function testEtchSymbolicAddr(address who) public { + // function check_etch_SymbolicAddr(address who) public { // vm.etch(who, hex"60425f526001601ff3"); // (bool success, bytes memory retval) = who.call(""); @@ -62,7 +79,7 @@ contract FoundryTest is Test { // } /// @notice etching symbolic code is not supported - // function testEtchFullSymbolic(address who, bytes memory code) public { + // function check_etch_FullSymbolic(address who, bytes memory code) public { // vm.etch(who, code); // assertEq(code, who.code); // } diff --git a/tests/regression/test/Getter.t.sol b/tests/regression/test/Getter.t.sol new file mode 100644 index 00000000..9eb9bdcf --- /dev/null +++ b/tests/regression/test/Getter.t.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +// from https://github.com/a16z/halmos/issues/82 + +/// @custom:halmos --storage-layout=generic +contract GetterTest { + uint256[3] public v; + uint w; + + function check_Getter(uint256 i) public view { + assert(v[i] >= 0); + } + + function check_externalGetter(uint256 i) public view { + assert(this.v(i) >= 0); + } +} diff --git a/tests/regression/test/HalmosCheatCode.t.sol b/tests/regression/test/HalmosCheatCode.t.sol new file mode 100644 index 00000000..ee928f81 --- /dev/null +++ b/tests/regression/test/HalmosCheatCode.t.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {SymTest} from "halmos-cheatcodes/SymTest.sol"; + +contract HalmosCheatCodeTest is SymTest { + function check_createUint() public { + uint x = svm.createUint(256, 'x'); + uint y = svm.createUint(160, 'y'); + uint z = svm.createUint(8, 'z'); + assert(0 <= x && x <= type(uint256).max); + assert(0 <= y && y <= type(uint160).max); + assert(0 <= z && z <= type(uint8).max); + } + + function check_createInt() public { + int x = svm.createInt(256, 'x'); + int y = svm.createInt(160, 'y'); + int z = svm.createInt(8, 'z'); + assert(type(int256).min <= x && x <= type(int256).max); + assert(type(int160).min <= y && y <= type(int160).max); + assert(type(int8).min <= z && z <= type(int8).max); + } + + function check_createBytes() public { + bytes memory data = svm.createBytes(2, 'data'); + uint x = uint(uint8(data[0])); + uint y = uint(uint8(data[1])); + assert(0 <= x && x <= type(uint8).max); + assert(0 <= y && y <= type(uint8).max); + } + + function check_createString() public { + string memory data = svm.createString(5, 'str'); + assert(bytes(data).length == 5); + } + + function check_createUint256() public { + uint x = svm.createUint256('x'); + assert(0 <= x && x <= type(uint256).max); + } + + function check_createInt256() public { + int x = svm.createInt256('x'); + assert(type(int256).min <= x && x <= type(int256).max); + } + + function check_createBytes32() public { + bytes32 x = svm.createBytes32('x'); + assert(0 <= uint(x) && uint(x) <= type(uint256).max); + uint y; assembly { y := x } + assert(0 <= y && y <= type(uint256).max); + } + + function check_createBytes4() public { + bytes4 x = svm.createBytes4('x'); + uint256 x_uint = uint256(uint32(x)); + assert(0 <= x_uint && x_uint <= type(uint32).max); + uint y; assembly { y := x } + assert(0 <= y && y <= type(uint256).max); + } + + function check_createAddress() public { + address x = svm.createAddress('x'); + uint y; assembly { y := x } + assert(0 <= y && y <= type(uint160).max); + } + + function check_createBool() public { + bool x = svm.createBool('x'); + uint y; assembly { y := x } + assert(y == 0 || y == 1); + } + + function check_SymbolLabel() public returns (uint256) { + uint x = svm.createUint256(''); + uint y = svm.createUint256(' '); + uint z = svm.createUint256(' a '); + uint w = svm.createUint256(' a b '); + return x + y + z + w; + } + + function check_FailUnknownCheatcode() public { + Dummy(address(svm)).foo(); // expected to fail with unknown cheatcode + } +} + +interface Dummy { + function foo() external; +} diff --git a/tests/test/Invalid.t.sol b/tests/regression/test/Invalid.t.sol similarity index 59% rename from tests/test/Invalid.t.sol rename to tests/regression/test/Invalid.t.sol index 069377e6..2014ec6f 100644 --- a/tests/test/Invalid.t.sol +++ b/tests/regression/test/Invalid.t.sol @@ -3,13 +3,17 @@ pragma solidity ^0.5.2; contract OldCompilerTest { - function testAssert(uint x) public { + function check_assert(uint x) public pure { if (x == 0) return; assert(false); // old compiler versions don't revert with panic; instead, they run invalid opcode, which halmos ignores, resulting in no error here. - //myAssert(false); // you can use your own assertion that panic-reverts if assertion fails, when using halmos for old version code. } - function myAssert(bool cond) internal { + function check_myAssert(uint x) public pure { + if (x == 0) return; + myAssert(false); // you can use your own assertion that panic-reverts if assertion fails, when using halmos for old version code. + } + + function myAssert(bool cond) internal pure { if (!cond) { assembly { mstore(0x00, 0x4e487b71) diff --git a/tests/test/Library.t.sol b/tests/regression/test/Library.t.sol similarity index 91% rename from tests/test/Library.t.sol rename to tests/regression/test/Library.t.sol index 662d7790..e64bbb04 100644 --- a/tests/test/Library.t.sol +++ b/tests/regression/test/Library.t.sol @@ -14,7 +14,7 @@ library Math { } contract LibraryTest { - function testAdd(uint x, uint y) public pure { + function check_add(uint x, uint y) public pure { unchecked { assert(Math._add(x,y) == x+y); /* TODO: support public library functions (library linking) diff --git a/tests/regression/test/LibraryLinking.t.sol b/tests/regression/test/LibraryLinking.t.sol new file mode 100644 index 00000000..1849f7b3 --- /dev/null +++ b/tests/regression/test/LibraryLinking.t.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +library Lib { + function foo() public pure returns (uint) { return 1; } + function bar() internal pure returns (uint) { return 2; } +} + +contract LibTest { + function check_foo() public pure { + assert(Lib.foo() == 1); // library linking placeholder error + } +} + +contract LibTest2 { + function check_bar() public pure { + assert(Lib.bar() == 2); // this is fine because internal library functions are inlined + } +} diff --git a/tests/test/List.t.sol b/tests/regression/test/List.t.sol similarity index 81% rename from tests/test/List.t.sol rename to tests/regression/test/List.t.sol index 8a9130a3..210f4c2b 100644 --- a/tests/test/List.t.sol +++ b/tests/regression/test/List.t.sol @@ -4,8 +4,9 @@ pragma solidity >=0.8.0 <0.9.0; import "forge-std/Test.sol"; import "../src/List.sol"; +/// @custom:halmos --symbolic-storage contract ListTest is Test, List { - function testAdd(uint x) public { + function check_add(uint x) public { uint oldSize = arr.length; vm.assume(oldSize < type(uint).max); add(x); @@ -15,7 +16,7 @@ contract ListTest is Test, List { assert(arr[newSize-1] == x); } - function testRemove() public { + function check_remove() public { uint oldSize = arr.length; vm.assume(oldSize > 0); remove(); @@ -24,13 +25,14 @@ contract ListTest is Test, List { assert(oldSize == newSize + 1); } - function testSet(uint i, uint x) public { + function check_set(uint i, uint x) public { vm.assume(i < arr.length); set(i, x); assert(arr[i] == x); } } +/// @custom:halmos --symbolic-storage contract ListTestTest is Test { List list; @@ -39,7 +41,7 @@ contract ListTestTest is Test { list.add(1); } - function testAdd(uint x) public { + function check_add(uint x) public { uint oldSize = list.size(); vm.assume(oldSize < type(uint).max); list.add(x); @@ -49,7 +51,7 @@ contract ListTestTest is Test { assert(list.arr(newSize-1) == x); } - function testRemove() public { + function check_remove() public { uint oldSize = list.size(); vm.assume(oldSize > 0); list.remove(); @@ -58,7 +60,7 @@ contract ListTestTest is Test { assert(oldSize == newSize + 1); } - function testSet(uint i, uint x) public { + function check_set(uint i, uint x) public { vm.assume(i < list.size()); list.set(i, x); assert(list.arr(i) == x); diff --git a/tests/regression/test/Natspec.t.sol b/tests/regression/test/Natspec.t.sol new file mode 100644 index 00000000..1e4000bd --- /dev/null +++ b/tests/regression/test/Natspec.t.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +contract NatspecTestNone { + Loop l; + + function setUp() public { + l = new Loop(); + } + + function check_Loop2(uint n) public view { + assert(l.iter(n) <= 2); // pass // default + } + function check_Loop2Fail(uint n) public view { + assert(l.iter(n) <= 1); // fail // default + } +} + +/// @custom:halmos --loop 3 +contract NatspecTestContract { + Loop l; + + function setUp() public { + l = new Loop(); + } + + function check_Loop3(uint n) public view { + assert(l.iter(n) <= 3); // pass // inherited from contract + } + function check_Loop3Fail(uint n) public view { + assert(l.iter(n) <= 2); // fail // inherited from contract + } +} + +contract NatspecTestSetup { + Loop l; + + /// @custom:halmos --loop 3 + function setUp() public { + l = new Loop(); + } + + function check_Loop2(uint n) public view { + assert(l.iter(n) <= 2); // pass // default + } + function check_Loop2Fail(uint n) public view { + assert(l.iter(n) <= 1); // fail // default + } +} + +contract NatspecTestFunction { + Loop l; + + function setUp() public { + l = new Loop(); + } + + /// @custom:halmos --loop 3 + function check_Loop3(uint n) public view { + assert(l.iter(n) <= 3); // pass + } + /// @custom:halmos --loop 3 + function check_Loop3Fail(uint n) public view { + assert(l.iter(n) <= 2); // fail + } + + function check_Loop2(uint n) public view { + assert(l.iter(n) <= 2); // pass // default + } + function check_Loop2Fail(uint n) public view { + assert(l.iter(n) <= 1); // fail // default + } +} + +/// @custom:halmos --loop 4 +contract NatspecTestOverwrite { + Loop l; + + function setUp() public { + l = new Loop(); + } + + function check_Loop4(uint n) public view { + assert(l.iter(n) <= 4); // pass // inherited from contract + } + function check_Loop4Fail(uint n) public view { + assert(l.iter(n) <= 3); // fail // inherited from contract + } + + /// @custom:halmos --loop 3 + function check_Loop3(uint n) public view { + assert(l.iter(n) <= 3); // pass // overwrite + } + /// @custom:halmos --loop 3 + function check_Loop3Fail(uint n) public view { + assert(l.iter(n) <= 2); // fail // overwrite + } +} + +contract Loop { + function iter(uint n) public pure returns (uint) { + uint cnt = 0; + for (uint i = 0; i < n; i++) { + cnt++; + } + return cnt; + } +} diff --git a/tests/test/Opcode.t.sol b/tests/regression/test/Opcode.t.sol similarity index 75% rename from tests/test/Opcode.t.sol rename to tests/regression/test/Opcode.t.sol index 06ce427f..e75dae4a 100644 --- a/tests/test/Opcode.t.sol +++ b/tests/regression/test/Opcode.t.sol @@ -13,23 +13,23 @@ library Opcode { contract OpcodeTest is Test { - function test_SIGNEXTEND(uint value) public { - _test_SIGNEXTEND(0, value); - _test_SIGNEXTEND(1, value); - _test_SIGNEXTEND(2, value); - _test_SIGNEXTEND(30, value); - _test_SIGNEXTEND(31, value); - _test_SIGNEXTEND(32, value); - _test_SIGNEXTEND(33, value); + function check_SIGNEXTEND(uint value) public { + _check_SIGNEXTEND(0, value); + _check_SIGNEXTEND(1, value); + _check_SIGNEXTEND(2, value); + _check_SIGNEXTEND(30, value); + _check_SIGNEXTEND(31, value); + _check_SIGNEXTEND(32, value); + _check_SIGNEXTEND(33, value); } /* TODO: support symbolic size - function test_SIGNEXTEND(uint size, uint value) public { - _test_SIGNEXTEND(size, value); + function check_SIGNEXTEND(uint size, uint value) public { + _check_SIGNEXTEND(size, value); } */ - function _test_SIGNEXTEND(uint size, uint value) public { + function _check_SIGNEXTEND(uint size, uint value) public { uint result1 = Opcode.SIGNEXTEND(size, value); uint result2; if (size > 31) { @@ -46,7 +46,7 @@ contract OpcodeTest is Test { assertEq(result1, result2); } - function testPush0() public { + function check_PUSH0() public { // target bytecode is 0x365f5f37365ff3 // 36 CALLDATASIZE // 5F PUSH0 diff --git a/tests/test/Prank.t.sol b/tests/regression/test/Prank.t.sol similarity index 70% rename from tests/test/Prank.t.sol rename to tests/regression/test/Prank.t.sol index 7f0ac003..1da70d52 100644 --- a/tests/test/Prank.t.sol +++ b/tests/regression/test/Prank.t.sol @@ -17,6 +17,14 @@ contract Target { } } +contract ConstructorRecorder { + address public caller; + + constructor() { + caller = msg.sender; + } +} + contract Ext is Test { function prank(address user) public { vm.prank(user); @@ -31,7 +39,7 @@ contract PrankSetUpTest is Test { vm.prank(address(target)); // prank is reset after setUp() } - function testPrank(address user) public { + function check_prank(address user) public { vm.prank(user); target.recordCaller(); assert(target.caller() == user); @@ -52,7 +60,7 @@ contract PrankTest is Test { vm.prank(user); } - function testPrank(address user) public { + function check_prank(address user) public { vm.prank(user); target.recordCaller(); assert(target.caller() == user); @@ -61,7 +69,7 @@ contract PrankTest is Test { assert(target.caller() == address(this)); } - function testStartPrank(address user) public { + function check_startPrank(address user) public { vm.startPrank(user); target.recordCaller(); @@ -79,25 +87,25 @@ contract PrankTest is Test { assert(target.caller() == address(this)); } - function testPrankInternal(address user) public { + function check_prank_Internal(address user) public { prank(user); // indirect prank target.recordCaller(); assert(target.caller() == user); } - function testPrankExternal(address user) public { + function check_prank_External(address user) public { ext.prank(user); // prank isn't propagated beyond the vm boundry target.recordCaller(); assert(target.caller() == address(this)); } - function testPrankExternalSelf(address user) public { + function check_prank_ExternalSelf(address user) public { this.prank(user); // prank isn't propagated beyond the vm boundry target.recordCaller(); assert(target.caller() == address(this)); } - function testPrankNew(address user) public { + function check_prank_New(address user) public { vm.prank(user); dummy = new Dummy(); // contract creation also consumes prank vm.prank(user); @@ -105,30 +113,43 @@ contract PrankTest is Test { assert(target.caller() == user); } - function testPrankReset1(address user) public { + function check_prank_Reset1(address user) public { // vm.prank(address(target)); // overwriting active prank is not allowed vm.prank(user); target.recordCaller(); assert(target.caller() == user); } - function testPrankReset2(address user) public { + function check_prank_Reset2(address user) public { // vm.prank(address(target)); // overwriting active prank is not allowed vm.startPrank(user); target.recordCaller(); assert(target.caller() == user); } - function testStopPrank1(address user) public { + function check_stopPrank_1(address user) public { vm.prank(user); vm.stopPrank(); // stopPrank can be used to disable both startPrank() and prank() target.recordCaller(); assert(target.caller() == address(this)); } - function testStopPrank2() public { + function check_stopPrank_2() public { vm.stopPrank(); // stopPrank is allowed even when no active prank exists! target.recordCaller(); assert(target.caller() == address(this)); } + + function check_prank_Constructor(address user) public { + vm.prank(user); + ConstructorRecorder recorder = new ConstructorRecorder(); + assert(recorder.caller() == user); + } + + // TODO: uncomment when we add CREATE2 support + // function check_prank_ConstructorCreate2(address user, bytes32 salt) public { + // vm.prank(user); + // ConstructorRecorder recorder = new ConstructorRecorder{salt:salt}(); + // assert(recorder.caller() == user); + // } } diff --git a/tests/regression/test/Proxy.t.sol b/tests/regression/test/Proxy.t.sol new file mode 100644 index 00000000..984c6b6c --- /dev/null +++ b/tests/regression/test/Proxy.t.sol @@ -0,0 +1,35 @@ +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; + +import {ERC1967Proxy} from "openzeppelin/proxy/ERC1967/ERC1967Proxy.sol"; + +contract C { + uint public num; + function foo(uint x) public payable returns (address, uint, address) { + num = x; + return (msg.sender, msg.value, address(this)); + } +} + +contract ProxyTest is Test { + C cImpl; + C c; + + function setUp() public { + cImpl = new C(); + c = C(address(new ERC1967Proxy(address(cImpl), ""))); + } + + function check_foo(uint x, uint fund, address caller) public { + vm.deal(caller, fund); + vm.prank(caller); + (address msg_sender, uint msg_value, address target) = c.foo{ value: fund }(x); + assert(msg_sender == caller); + assert(msg_value == fund); + assert(target == address(c)); + + assert(c.num() == x); + assert(cImpl.num() == 0); + } +} diff --git a/tests/regression/test/Reset.t.sol b/tests/regression/test/Reset.t.sol new file mode 100644 index 00000000..b483cba7 --- /dev/null +++ b/tests/regression/test/Reset.t.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +contract C { + function foo() public pure returns (uint) { + return 1; + } +} + +/// @custom:halmos --reset-bytecode 0xaaaa0002=0x6080604052348015600f57600080fd5b506004361060285760003560e01c8063c298557814602d575b600080fd5b600260405190815260200160405180910390f3fea2646970667358221220c2880ecd3d663c2d8a036163ee7c5d65b9a7d1749e1132fd8ff89646c6621d5764736f6c63430008130033 +contract ResetTest { + C c; + + function setUp() public { + c = new C(); + } + + function check_foo() public view { + assert(c.foo() == 2); // for testing --reset-bytecode option + } + +} diff --git a/tests/regression/test/Revert.t.sol b/tests/regression/test/Revert.t.sol new file mode 100644 index 00000000..f72f3e1a --- /dev/null +++ b/tests/regression/test/Revert.t.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +// from https://github.com/a16z/halmos/issues/109 + +import "forge-std/Test.sol"; + +contract A { } + +contract C { + uint256 public num; + + function set1(uint256 x) public { + num = x; + revert("blah"); + } + + function set2(uint256 x) public { + revert("blah"); + num = x; + } + + function deposit(bool paused) public payable { + if (paused) revert("paused"); + } + + function create() public { + A a = new A(); + revert(string(abi.encode(a))); + } +} + +contract CTest is Test { + C c; + + function setUp() public { + c = new C(); + } + + function check_Revert1(uint256 x) public { + require(x != 0); + (bool result, ) = address(c).call(abi.encodeWithSignature("set1(uint256)", x)); + assert(!result); + assert(c.num() != x); + } + + function check_Revert2(uint256 x) public { + require(x != 0); + (bool result, ) = address(c).call(abi.encodeWithSignature("set2(uint256)", x)); + assert(!result); + assert(c.num() != x); + } + + function check_RevertBalance(bool paused, uint256 amount) public { + vm.deal(address(this), amount); + vm.deal(address(c), 0); + + (bool result,) = address(c).call{value: amount}(abi.encodeWithSignature("deposit(bool)", paused)); + + if (result) { + assert(!paused); + assert(address(this).balance == 0); + assert(address(c).balance == amount); + } else { + assert(paused); + assert(address(this).balance == amount); + assert(address(c).balance == 0); + } + } + + function codesize(address x) internal view returns (uint256 size) { + assembly { size := extcodesize(x) } + } + + function check_RevertCode(address x) public { + uint256 oldSize = codesize(x); + try c.create() { + } catch Error(string memory s) { + address a = abi.decode(bytes(s), (address)); + uint256 size = codesize(a); + vm.assume(a == x); + assert(size == oldSize); + } + } +} diff --git a/tests/regression/test/Send.t.sol b/tests/regression/test/Send.t.sol new file mode 100644 index 00000000..2b7ab546 --- /dev/null +++ b/tests/regression/test/Send.t.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +contract C { + constructor() payable { } +} + +/// @custom:halmos --solver-timeout-assertion 0 +contract SendTest { + + function check_transfer(address payable receiver, uint amount, address others) public { + require(others != address(this) && others != receiver); + + uint oldBalanceSender = address(this).balance; + uint oldBalanceReceiver = receiver.balance; + uint oldBalanceOthers = others.balance; + + receiver.transfer(amount); + + assert(oldBalanceSender >= amount); + + uint newBalanceSender = address(this).balance; + uint newBalanceReceiver = receiver.balance; + uint newBalanceOthers = others.balance; + + unchecked { + if (receiver != address(this)) { + assert(newBalanceSender == oldBalanceSender - amount); + assert(newBalanceSender <= oldBalanceSender); + assert(newBalanceReceiver == oldBalanceReceiver + amount); + assert(newBalanceReceiver >= oldBalanceReceiver); + } else { + assert(newBalanceSender == oldBalanceSender); + assert(newBalanceReceiver == oldBalanceReceiver); + } + assert(oldBalanceSender + oldBalanceReceiver == newBalanceSender + newBalanceReceiver); + assert(oldBalanceOthers == newBalanceOthers); + } + } + + function check_send(address payable receiver, uint amount, address others, uint mode) public { + require(others != address(this) && others != receiver); + + uint oldBalanceSender = address(this).balance; + uint oldBalanceReceiver = receiver.balance; + uint oldBalanceOthers = others.balance; + + bool success; + if (mode == 0) { + success = receiver.send(amount); + } else { + (success,) = receiver.call{ value: amount }(""); + } + + if (success) { + assert(oldBalanceSender >= amount); + } + + uint newBalanceSender = address(this).balance; + uint newBalanceReceiver = receiver.balance; + uint newBalanceOthers = others.balance; + + unchecked { + if (success && receiver != address(this)) { + assert(newBalanceSender == oldBalanceSender - amount); + assert(newBalanceSender <= oldBalanceSender); + assert(newBalanceReceiver == oldBalanceReceiver + amount); + assert(newBalanceReceiver >= oldBalanceReceiver); + } else { + assert(newBalanceSender == oldBalanceSender); + assert(newBalanceReceiver == oldBalanceReceiver); + } + assert(oldBalanceSender + oldBalanceReceiver == newBalanceSender + newBalanceReceiver); + assert(oldBalanceOthers == newBalanceOthers); + } + } + + function check_create(address receiver, uint amount, address others, bytes32 salt, uint mode) public { + require(others != address(this) && others != receiver); + + uint oldBalanceSender = address(this).balance; + uint oldBalanceReceiver = receiver.balance; + uint oldBalanceOthers = others.balance; + + C c; + if (mode == 0) { + c = new C{ value: amount }(); + } else { + c = new C{ value: amount, salt: salt }(); + } + require(receiver == address(c)); + + assert(oldBalanceSender >= amount); + + uint newBalanceSender = address(this).balance; + uint newBalanceReceiver = receiver.balance; + uint newBalanceOthers = others.balance; + + unchecked { + assert(receiver != address(this)); // new address cannot be equal to the deployer + assert(newBalanceSender == oldBalanceSender - amount); + assert(newBalanceSender <= oldBalanceSender); + assert(newBalanceReceiver == oldBalanceReceiver + amount); + assert(newBalanceReceiver >= oldBalanceReceiver); + assert(oldBalanceSender + oldBalanceReceiver == newBalanceSender + newBalanceReceiver); + assert(oldBalanceOthers == newBalanceOthers); + } + } + + receive() external payable {} +} diff --git a/tests/test/Setup.t.sol b/tests/regression/test/Setup.t.sol similarity index 70% rename from tests/test/Setup.t.sol rename to tests/regression/test/Setup.t.sol index bf814e9d..37b20d22 100644 --- a/tests/test/Setup.t.sol +++ b/tests/regression/test/Setup.t.sol @@ -15,9 +15,23 @@ contract SetupTest is Test { } } - function testTrue() public { + function check_True() public { assertEq(users[0], address(bytes20(keccak256("test")))); assertEq(users[1], address(uint160(users[0]) + 1)); assertEq(users[2], address(0)); } } + +contract SetupFailTest { + function setUp() public { + revert(); + } + + function check_setUp_Fail1() public { + assert(true); + } + + function check_setUp_Fail2() public { + assert(true); + } +} diff --git a/tests/test/SetupPlus.t.sol b/tests/regression/test/SetupPlus.t.sol similarity index 95% rename from tests/test/SetupPlus.t.sol rename to tests/regression/test/SetupPlus.t.sol index 3ff140d5..e527029f 100644 --- a/tests/test/SetupPlus.t.sol +++ b/tests/regression/test/SetupPlus.t.sol @@ -34,7 +34,7 @@ contract SetupPlusTest { a = new A(x, x); } - function testSetup() public { + function check_Setup() public view { assert(a.x() > 10); assert(a.y() > 100); } @@ -87,7 +87,7 @@ contract SetupPlusTestB { mk(); } - function testSetup() public { + function check_Setup() public view { assert(b.x1() == init[0]); assert(b.y1() == init[1]); assert(b.x2() == init[2]); diff --git a/tests/regression/test/SetupSymbolic.t.sol b/tests/regression/test/SetupSymbolic.t.sol new file mode 100644 index 00000000..78d44af7 --- /dev/null +++ b/tests/regression/test/SetupSymbolic.t.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +contract SetupSymbolicTest { + function setUpSymbolic(uint x) public pure { + if (x > 0) return; // generate multiple setup output states + } + + function check_True() public pure { + assert(true); // ensure setUp succeeds + } +} diff --git a/tests/regression/test/Sha3.t.sol b/tests/regression/test/Sha3.t.sol new file mode 100644 index 00000000..b4353e52 --- /dev/null +++ b/tests/regression/test/Sha3.t.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import "forge-std/Test.sol"; +import {SymTest} from "halmos-cheatcodes/SymTest.sol"; + +contract Sha3Test is Test, SymTest { + function check_hash() public { + _assert_eq("", ""); + _assert_eq("1", "1"); + + bytes memory data = svm.createBytes(1, "data"); + _assert_eq(data, data); + } + + function check_no_hash_collision_assumption() public { + // assume no hash collisions + + bytes memory data1 = svm.createBytes(1, "data1"); + bytes memory data2 = svm.createBytes(2, "data2"); + _assert_neq(data1, data2); + + bytes memory data32_1 = svm.createBytes(32, "data32_1"); + bytes memory data32_2 = svm.createBytes(32, "data32_2"); + vm.assume(keccak256(data32_1) == keccak256(data32_2)); + assert(data32_1[0] == data32_2[0]); + } + + function _assert_eq(bytes memory data1, bytes memory data2) internal { + assert(keccak256(data1) == keccak256(data2)); + } + + function _assert_neq(bytes memory data1, bytes memory data2) internal { + assert(keccak256(data1) != keccak256(data2)); + } +} diff --git a/tests/test/SignExtend.t.sol b/tests/regression/test/SignExtend.t.sol similarity index 81% rename from tests/test/SignExtend.t.sol rename to tests/regression/test/SignExtend.t.sol index ec56c455..c7ca28b1 100644 --- a/tests/test/SignExtend.t.sol +++ b/tests/regression/test/SignExtend.t.sol @@ -5,7 +5,7 @@ import "forge-std/Test.sol"; import "../src/SignExtend.sol"; contract SignExtendTest is SignExtend { - function testSignExtend(int16 _x) public pure { + function check_SIGNEXTEND(int16 _x) public pure { int16 x = changeMySign(_x); assert(x == -_x); } diff --git a/tests/regression/test/Signature.t.sol b/tests/regression/test/Signature.t.sol new file mode 100644 index 00000000..bc9c9ea6 --- /dev/null +++ b/tests/regression/test/Signature.t.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {SymTest} from "halmos-cheatcodes/SymTest.sol"; +import {Test} from "forge-std/Test.sol"; + +import {SignatureChecker} from "openzeppelin/utils/cryptography/SignatureChecker.sol"; +import {ECDSA} from "openzeppelin/utils/cryptography/ECDSA.sol"; + +contract SymAccount is SymTest { + fallback(bytes calldata) external payable returns (bytes memory) { + uint mode = svm.createUint256("mode"); + if (mode == 0) { + return ""; // simulate empty code + } else if (mode == 1) { + return svm.createBytes(32, "retdata32"); // any primitive return value: bool, address, uintN, bytesN, etc + } else if (mode == 2) { + return svm.createBytes(64, "retdata64"); // two primitive return values + } else { + revert(); // simulate no fallback + } + } +} + +contract SignatureTest is SymTest, Test { + function check_isValidSignatureNow(bytes32 hash, bytes memory signature) public { + address signer = address(new SymAccount()); + if (!SignatureChecker.isValidSignatureNow(signer, hash, signature)) revert(); + assert(true); + } + + function check_isValidERC1271SignatureNow(bytes32 hash, bytes memory signature) public { + address signer = address(new SymAccount()); + if (!SignatureChecker.isValidERC1271SignatureNow(signer, hash, signature)) revert(); + assert(true); + } + + function check_tryRecover(address signer, bytes32 hash, bytes memory signature) public { + (address recovered, ECDSA.RecoverError error, ) = ECDSA.tryRecover(hash, signature); + if (!(error == ECDSA.RecoverError.NoError && recovered == signer)) revert(); + assert(true); + } + + function check_ecrecover( + bytes32 hash, + uint8 v, + bytes32 r, + bytes32 s + ) public { + address signer = ecrecover(hash, v, r, s); + if (signer == address(0)) revert(); + assert(true); + } +} diff --git a/tests/regression/test/SmolWETH.t.sol b/tests/regression/test/SmolWETH.t.sol new file mode 100644 index 00000000..c8928575 --- /dev/null +++ b/tests/regression/test/SmolWETH.t.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import "forge-std/Test.sol"; +import {SymTest} from "halmos-cheatcodes/SymTest.sol"; + +contract Dummy {} + +/// @notice don't use, this is very buggy on purpose +contract SmolWETH { + function deposit() external payable { + // bug: we stomp any existing balance + assembly { + sstore(caller(), callvalue()) + } + } + + function withdraw(uint256) external { + assembly { + // revert if msg.value > 0 + if gt(callvalue(), 0) { + revert(0, 0) + } + + let amount := sload(caller()) + let success := call(gas(), caller(), amount, 0, 0, 0, 0) + + // bug: we always erase the balance, regardless of transfer success + // bug: we should erase the balance before making the call + sstore(caller(), 0) + } + } + + function balanceOf(address account) external view returns (uint256) { + assembly { + mstore(0, sload(account)) + return(0, 0x20) + } + } +} + +/// @custom:halmos --storage-layout=generic +contract SmolWETHTest is Test, SymTest { + SmolWETH weth; + + function setUp() public { + weth = new SmolWETH(); + } + + function check_deposit_once(address alice, uint256 amount) external { + // fund alice + vm.deal(alice, amount); + + // alice deposits + vm.prank(alice); + weth.deposit{value: amount}(); + + // alice's balance is updated + assertEq(weth.balanceOf(alice), amount); + } +} diff --git a/tests/regression/test/StaticContexts.t.sol b/tests/regression/test/StaticContexts.t.sol new file mode 100644 index 00000000..d63f3da1 --- /dev/null +++ b/tests/regression/test/StaticContexts.t.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import "forge-std/Test.sol"; + +contract Dummy {} + +contract StaticContextsTest is Test { + event Log(uint256 x); + + uint256 x; + + function do_sstore() public { + unchecked { + x += 1; + } + } + + function do_log() public { + emit Log(x); + } + + function do_create() public { + new Dummy(); + } + + function do_create2() public { + new Dummy{salt: 0}(); + } + + function do_call_with_value() public { + vm.deal(address(this), 1 ether); + (bool success, ) = payable(address(this)).call{value: 1 ether}(""); + success; // silence warnings + } + + function do_selfdestruct() public { + selfdestruct(payable(address(0))); + } + + function check_sstore_fails() public { + (bool success, ) = address(this).staticcall(abi.encodeWithSignature("do_sstore()")); + assertFalse(success); + } + + function check_log_fails() public { + (bool success, ) = address(this).staticcall(abi.encodeWithSignature("do_log()")); + assertFalse(success); + } + + function check_create_fails() public { + (bool success, ) = address(this).staticcall(abi.encodeWithSignature("do_create()")); + assertFalse(success); + } + + function check_create2_fails() public { + (bool success, ) = address(this).staticcall(abi.encodeWithSignature("do_create2()")); + assertFalse(success); + } + + // TODO: value check not implemented yet + // function check_call_with_value_fails() public { + // (bool success, ) = address(this).staticcall(abi.encodeWithSignature("do_call_with_value()")); + // assertFalse(success); + // } + + // TODO: selfdestruct not implemented yet + // function check_selfdestruct_fails() public { + // (bool success, ) = address(this).staticcall(abi.encodeWithSignature("do_selfdestruct()")); + // assertFalse(success); + // } +} diff --git a/tests/test/Storage.t.sol b/tests/regression/test/Storage.t.sol similarity index 68% rename from tests/test/Storage.t.sol rename to tests/regression/test/Storage.t.sol index 3b0372fe..f8b106d5 100644 --- a/tests/test/Storage.t.sol +++ b/tests/regression/test/Storage.t.sol @@ -4,37 +4,38 @@ pragma solidity >=0.8.0 <0.9.0; import "forge-std/Test.sol"; import "../src/Storage.sol"; +/// @custom:halmos --symbolic-storage contract StorageTest is Storage { - function testSetMap1(uint k, uint v) public { + function check_setMap1(uint k, uint v) public { setMap1(k, v); assert(map1[k] == v); } - function testSetMap2(uint k1, uint k2, uint v) public { + function check_setMap2(uint k1, uint k2, uint v) public { setMap2(k1, k2, v); assert(map2[k1][k2] == v); } - function testSetMap3(uint k1, uint k2, uint k3, uint v) public { + function check_setMap3(uint k1, uint k2, uint k3, uint v) public { setMap3(k1, k2, k3, v); assert(map3[k1][k2][k3] == v); } - function testAddArr1(uint v) public { + function check_addArr1(uint v) public { uint size = arr1.length; addArr1(v); assert(arr1.length == size + 1); assert(arr1[size] == v); } - function testAddArr2(uint i, uint v) public { + function check_addArr2(uint i, uint v) public { uint size = arr2[i].length; addArr2(i, v); assert(arr2[i].length == size + 1); assert(arr2[i][size] == v); } - function testAddMap1Arr1(uint k, uint v) public { + function check_addMap1Arr1(uint k, uint v) public { uint size = map1Arr1[k].length; addMap1Arr1(k, v); assert(map1Arr1[k].length == size + 1); diff --git a/tests/regression/test/Storage2.t.sol b/tests/regression/test/Storage2.t.sol new file mode 100644 index 00000000..f1b66a09 --- /dev/null +++ b/tests/regression/test/Storage2.t.sol @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import "forge-std/Test.sol"; + +contract Storage { + struct Tuple1 { + uint x; + } + + struct Tuple2 { + uint y; + uint z; + } + + // m[k].f: #(k.m)+f km.f + mapping (uint => uint) public map1_uint; + mapping (uint => Tuple1) public map1_tuple1; + mapping (uint => Tuple2) public map1_tuple2; + + // m[k][l].f: #(l.#(k.m))+f lkm.0.f + mapping (uint => mapping (uint => uint)) public map2_uint; + mapping (uint => mapping (uint => Tuple1)) public map2_tuple1; + mapping (uint => mapping (uint => Tuple2)) public map2_tuple2; + + // a[i].f: #(a)+i*n+f a._ + uint[] public arr1_uint; + Tuple1[] public arr1_tuple1; + Tuple2[] public arr1_tuple2; + + // a[i][j].f: #(#(a)+i)+j*n+f a.i._ + uint[][] public arr2_uint; + Tuple1[][] public arr2_tuple1; + Tuple2[][] public arr2_tuple2; + + // m[k][i].f: #(#(k.m))+i*n+f km.0._ + mapping (uint => uint[]) public map1_arr1_uint; + mapping (uint => Tuple1[]) public map1_arr1_tuple1; + mapping (uint => Tuple2[]) public map1_arr1_tuple2; + + // a[i][k].f: #(k.(#(a)+i))+f ka.i.f + mapping (uint => uint)[] public arr1_map1_uint; + mapping (uint => Tuple1)[] public arr1_map1_tuple1; + mapping (uint => Tuple2)[] public arr1_map1_tuple2; + + // a[i][k][j].f: #(#(k.(#(a)+i)))+j*n+f ka.i.0._ + mapping (uint => uint[])[] public arr1_map1_arr1_uint; + mapping (uint => Tuple1[])[] public arr1_map1_arr1_tuple1; + mapping (uint => Tuple2[])[] public arr1_map1_arr1_tuple2; + + constructor () { } + + function maxsize_arr1() public { + assembly { + sstore(arr1_uint.slot, not(0)) + sstore(arr1_tuple1.slot, not(0)) + sstore(arr1_tuple2.slot, not(0)) + } + } + + function maxsize_arr2() public { + assembly { + sstore(arr2_uint.slot, not(0)) + sstore(arr2_tuple1.slot, not(0)) + sstore(arr2_tuple2.slot, not(0)) + } + } + + function maxsize_arr2(uint i) public { + uint[] storage arr2_uint_i = arr2_uint[i]; + Tuple1[] storage arr2_tuple1_i = arr2_tuple1[i]; + Tuple2[] storage arr2_tuple2_i = arr2_tuple2[i]; + assembly { + sstore(arr2_uint_i.slot, not(0)) + sstore(arr2_tuple1_i.slot, not(0)) + sstore(arr2_tuple2_i.slot, not(0)) + } + } + + function maxsize_map1_arr1(uint k) public { + uint[] storage map1_arr1_uint_k = map1_arr1_uint[k]; + Tuple1[] storage map1_arr1_tuple1_k = map1_arr1_tuple1[k]; + Tuple2[] storage map1_arr1_tuple2_k = map1_arr1_tuple2[k]; + assembly { + sstore(map1_arr1_uint_k.slot, not(0)) + sstore(map1_arr1_tuple1_k.slot, not(0)) + sstore(map1_arr1_tuple2_k.slot, not(0)) + } + } + + function maxsize_arr1_map1() public { + assembly { + sstore(arr1_map1_uint.slot, not(0)) + sstore(arr1_map1_tuple1.slot, not(0)) + sstore(arr1_map1_tuple2.slot, not(0)) + } + } + + function maxsize_arr1_map1_arr1() public { + assembly { + sstore(arr1_map1_arr1_uint.slot, not(0)) + sstore(arr1_map1_arr1_tuple1.slot, not(0)) + sstore(arr1_map1_arr1_tuple2.slot, not(0)) + } + } + + function maxsize_arr1_map1_arr1(uint i, uint k) public { + uint[] storage arr1_map1_arr1_uint_i_k = arr1_map1_arr1_uint[i][k]; + Tuple1[] storage arr1_map1_arr1_tuple1_i_k = arr1_map1_arr1_tuple1[i][k]; + Tuple2[] storage arr1_map1_arr1_tuple2_i_k = arr1_map1_arr1_tuple2[i][k]; + assembly { + sstore(arr1_map1_arr1_uint_i_k.slot, not(0)) + sstore(arr1_map1_arr1_tuple1_i_k.slot, not(0)) + sstore(arr1_map1_arr1_tuple2_i_k.slot, not(0)) + } + } +} + +contract Storage2Test is Storage, Test { + struct Param { + uint k11; uint v11; + uint k12; uint v12; + uint k13; uint v13; + uint k14; uint v14; + + uint k21; uint l21; uint v21; + uint k22; uint l22; uint v22; + uint k23; uint l23; uint v23; + uint k24; uint l24; uint v24; + + uint32 i31; uint v31; + uint32 i32; uint v32; + uint32 i33; uint v33; + uint32 i34; uint v34; + + uint32 i41; uint32 j41; uint v41; + uint32 i42; uint32 j42; uint v42; + uint32 i43; uint32 j43; uint v43; + uint32 i44; uint32 j44; uint v44; + + uint k51; uint32 i51; uint v51; + uint k52; uint32 i52; uint v52; + uint k53; uint32 i53; uint v53; + uint k54; uint32 i54; uint v54; + + uint32 i61; uint k61; uint v61; + uint32 i62; uint k62; uint v62; + uint32 i63; uint k63; uint v63; + uint32 i64; uint k64; uint v64; + + uint32 i71; uint k71; uint32 j71; uint v71; + uint32 i72; uint k72; uint32 j72; uint v72; + uint32 i73; uint k73; uint32 j73; uint v73; + uint32 i74; uint k74; uint32 j74; uint v74; + } + + function check_set(Param memory p) public { + map1_uint [p.k11] = p.v11; + map1_tuple1[p.k12].x = p.v12; + map1_tuple2[p.k13].y = p.v13; + map1_tuple2[p.k14].z = p.v14; + + map2_uint [p.k21][p.l21] = p.v21; + map2_tuple1[p.k22][p.l22].x = p.v22; + map2_tuple2[p.k23][p.l23].y = p.v23; + map2_tuple2[p.k24][p.l24].z = p.v24; + + maxsize_arr1(); + arr1_uint [p.i31] = p.v31; + arr1_tuple1[p.i32].x = p.v32; + arr1_tuple2[p.i33].y = p.v33; + arr1_tuple2[p.i34].z = p.v34; + + maxsize_arr2(); + maxsize_arr2(p.i41); + maxsize_arr2(p.i42); + maxsize_arr2(p.i43); + maxsize_arr2(p.i44); + arr2_uint [p.i41][p.j41] = p.v41; + arr2_tuple1[p.i42][p.j42].x = p.v42; + arr2_tuple2[p.i43][p.j43].y = p.v43; + arr2_tuple2[p.i44][p.j44].z = p.v44; + + maxsize_map1_arr1(p.k51); + maxsize_map1_arr1(p.k52); + maxsize_map1_arr1(p.k53); + maxsize_map1_arr1(p.k54); + map1_arr1_uint [p.k51][p.i51] = p.v51; + map1_arr1_tuple1[p.k52][p.i52].x = p.v52; + map1_arr1_tuple2[p.k53][p.i53].y = p.v53; + map1_arr1_tuple2[p.k54][p.i54].z = p.v54; + + maxsize_arr1_map1(); + arr1_map1_uint [p.i61][p.k61] = p.v61; + arr1_map1_tuple1[p.i62][p.k62].x = p.v62; + arr1_map1_tuple2[p.i63][p.k63].y = p.v63; + arr1_map1_tuple2[p.i64][p.k64].z = p.v64; + + maxsize_arr1_map1_arr1(); + maxsize_arr1_map1_arr1(p.i71, p.k71); + maxsize_arr1_map1_arr1(p.i72, p.k72); + maxsize_arr1_map1_arr1(p.i73, p.k73); + maxsize_arr1_map1_arr1(p.i74, p.k74); + arr1_map1_arr1_uint [p.i71][p.k71][p.j71] = p.v71; + arr1_map1_arr1_tuple1[p.i72][p.k72][p.j72].x = p.v72; + arr1_map1_arr1_tuple2[p.i73][p.k73][p.j73].y = p.v73; + arr1_map1_arr1_tuple2[p.i74][p.k74][p.j74].z = p.v74; + + // + + assert(map1_uint [p.k11] == p.v11); + assert(map1_tuple1[p.k12].x == p.v12); + assert(map1_tuple2[p.k13].y == p.v13); + assert(map1_tuple2[p.k14].z == p.v14); + + assert(map2_uint [p.k21][p.l21] == p.v21); + assert(map2_tuple1[p.k22][p.l22].x == p.v22); + assert(map2_tuple2[p.k23][p.l23].y == p.v23); + assert(map2_tuple2[p.k24][p.l24].z == p.v24); + + assert(arr1_uint [p.i31] == p.v31); + assert(arr1_tuple1[p.i32].x == p.v32); + assert(arr1_tuple2[p.i33].y == p.v33); + assert(arr1_tuple2[p.i34].z == p.v34); + + assert(arr2_uint [p.i41][p.j41] == p.v41); + assert(arr2_tuple1[p.i42][p.j42].x == p.v42); + assert(arr2_tuple2[p.i43][p.j43].y == p.v43); + assert(arr2_tuple2[p.i44][p.j44].z == p.v44); + + assert(map1_arr1_uint [p.k51][p.i51] == p.v51); + assert(map1_arr1_tuple1[p.k52][p.i52].x == p.v52); + assert(map1_arr1_tuple2[p.k53][p.i53].y == p.v53); + assert(map1_arr1_tuple2[p.k54][p.i54].z == p.v54); + + assert(arr1_map1_uint [p.i61][p.k61] == p.v61); + assert(arr1_map1_tuple1[p.i62][p.k62].x == p.v62); + assert(arr1_map1_tuple2[p.i63][p.k63].y == p.v63); + assert(arr1_map1_tuple2[p.i64][p.k64].z == p.v64); + + assert(arr1_map1_arr1_uint [p.i71][p.k71][p.j71] == p.v71); + assert(arr1_map1_arr1_tuple1[p.i72][p.k72][p.j72].x == p.v72); + assert(arr1_map1_arr1_tuple2[p.i73][p.k73][p.j73].y == p.v73); + assert(arr1_map1_arr1_tuple2[p.i74][p.k74][p.j74].z == p.v74); + } +} diff --git a/tests/regression/test/Storage3.t.sol b/tests/regression/test/Storage3.t.sol new file mode 100644 index 00000000..a99508bf --- /dev/null +++ b/tests/regression/test/Storage3.t.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {SymTest} from "halmos-cheatcodes/SymTest.sol"; +import {Test} from "forge-std/Test.sol"; + +contract Storage3 { + mapping(bytes1 => uint) map_bytes1; + + mapping(bytes => uint) map_bytes; + + mapping(uint => mapping(bytes => uint)) map_uint_bytes; + mapping(bytes => mapping(uint => uint)) map_bytes_uint; + + constructor () { } +} + +contract Storage3Test is Storage3, SymTest, Test { + struct Param { + bytes1 x0; uint v0; + + uint v11; + uint v12; + uint v13; + uint v14; + uint v15; + uint v16; + + uint y21; uint v21; + uint y22; uint v22; + uint y23; uint v23; + uint y24; uint v24; + uint y25; uint v25; + uint y26; uint v26; + + uint y31; uint v31; + uint y32; uint v32; + uint y33; uint v33; + uint y34; uint v34; + uint y35; uint v35; + uint y36; uint v36; + } + + function check_set(Param memory p) public { + bytes[] memory x = new bytes[](40); + + x[11] = svm.createBytes( 1, "x11"); + x[12] = svm.createBytes(31, "x12"); + x[13] = svm.createBytes(32, "x13"); + x[14] = svm.createBytes(33, "x14"); + x[15] = svm.createBytes(64, "x15"); + x[16] = svm.createBytes(65, "x16"); + + x[21] = svm.createBytes( 1, "x21"); + x[22] = svm.createBytes(31, "x22"); + x[23] = svm.createBytes(32, "x23"); + x[24] = svm.createBytes(33, "x24"); + x[25] = svm.createBytes(64, "x25"); + x[26] = svm.createBytes(65, "x26"); + + x[31] = svm.createBytes( 1, "x31"); + x[32] = svm.createBytes(31, "x32"); + x[33] = svm.createBytes(32, "x33"); + x[34] = svm.createBytes(33, "x34"); + x[35] = svm.createBytes(64, "x35"); + x[36] = svm.createBytes(65, "x36"); + + // + + map_bytes1[p.x0] = p.v0; + + map_bytes[x[11]] = p.v11; + map_bytes[x[12]] = p.v12; + map_bytes[x[13]] = p.v13; + map_bytes[x[14]] = p.v14; + map_bytes[x[15]] = p.v15; + map_bytes[x[16]] = p.v16; + + map_uint_bytes[p.y21][x[21]] = p.v21; + map_uint_bytes[p.y22][x[22]] = p.v22; + map_uint_bytes[p.y23][x[23]] = p.v23; + map_uint_bytes[p.y24][x[24]] = p.v24; + map_uint_bytes[p.y25][x[25]] = p.v25; + map_uint_bytes[p.y26][x[26]] = p.v26; + + map_bytes_uint[x[31]][p.y31] = p.v31; + map_bytes_uint[x[32]][p.y32] = p.v32; + map_bytes_uint[x[33]][p.y33] = p.v33; + map_bytes_uint[x[34]][p.y34] = p.v34; + map_bytes_uint[x[35]][p.y35] = p.v35; + map_bytes_uint[x[36]][p.y36] = p.v36; + + // + + assert(map_bytes1[p.x0] == p.v0); + + assert(map_bytes[x[11]] == p.v11); + assert(map_bytes[x[12]] == p.v12); + assert(map_bytes[x[13]] == p.v13); + assert(map_bytes[x[14]] == p.v14); + assert(map_bytes[x[15]] == p.v15); + assert(map_bytes[x[16]] == p.v16); + + assert(map_uint_bytes[p.y21][x[21]] == p.v21); + assert(map_uint_bytes[p.y22][x[22]] == p.v22); + assert(map_uint_bytes[p.y23][x[23]] == p.v23); + assert(map_uint_bytes[p.y24][x[24]] == p.v24); + assert(map_uint_bytes[p.y25][x[25]] == p.v25); + assert(map_uint_bytes[p.y26][x[26]] == p.v26); + + assert(map_bytes_uint[x[31]][p.y31] == p.v31); + assert(map_bytes_uint[x[32]][p.y32] == p.v32); + assert(map_bytes_uint[x[33]][p.y33] == p.v33); + assert(map_bytes_uint[x[34]][p.y34] == p.v34); + assert(map_bytes_uint[x[35]][p.y35] == p.v35); + assert(map_bytes_uint[x[36]][p.y36] == p.v36); + } +} diff --git a/tests/regression/test/Storage4.t.sol b/tests/regression/test/Storage4.t.sol new file mode 100644 index 00000000..7169e3c1 --- /dev/null +++ b/tests/regression/test/Storage4.t.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {SymTest} from "halmos-cheatcodes/SymTest.sol"; +import {Test} from "forge-std/Test.sol"; + +struct Set { + bytes[] _values; + mapping(bytes => uint256) _indexes; +} + +library EnumerableSet { + function add(Set storage set, bytes calldata value) internal returns (bool) { + if (!contains(set, value)) { + set._values.push(value); + set._indexes[value] = set._values.length; + return true; + } else { + return false; + } + } + + function contains(Set storage set, bytes calldata value) internal view returns (bool) { + return set._indexes[value] != 0; + } + + function length(Set storage set) internal view returns (uint256) { + return set._values.length; + } + + function at(Set storage set, uint256 index) internal view returns (bytes memory) { + return set._values[index]; + } + + function values(Set storage set) internal view returns (bytes[] memory) { + return set._values; + } +} + +contract Storage4 { + using EnumerableSet for Set; + + mapping(uint256 => Set) internal map; + + function add(uint256 key, bytes calldata value) external { + map[key].add(value); + } + + function lookup(uint256 key) internal view returns (Set storage) { + return map[key]; + } + + function totalValues(uint256 key) public view virtual returns (uint256) { + return lookup(key).length(); + } + + function valueAt(uint256 key, uint256 index) external view returns (bytes memory) { + return lookup(key).at(index); + } + + function valuesOf(uint256 key) external view returns (bytes[] memory) { + return lookup(key).values(); + } +} + + +contract Storage4Test is SymTest, Test { + Storage4 s; + + function setUp() public { + s = new Storage4(); + } + + function check_add_1(uint k) public { + bytes[] memory v = new bytes[](10); + + v[1] = svm.createBytes(31, "v1"); + v[2] = svm.createBytes(32, "v2"); + v[3] = svm.createBytes(33, "v3"); + + s.add(k, v[1]); + s.add(k, v[2]); + s.add(k, v[3]); + + assert(keccak256(s.valueAt(k, 0)) == keccak256(v[1])); + assert(keccak256(s.valueAt(k, 1)) == keccak256(v[2])); + assert(keccak256(s.valueAt(k, 2)) == keccak256(v[3])); + + assert(s.totalValues(k) == 3); + } + + function check_add_2(uint k) public { + bytes[] memory v = new bytes[](10); + + // note: v1 and v2 may be equal, since they are of the same size + v[1] = svm.createBytes(32, "v1"); + v[2] = svm.createBytes(32, "v2"); + + s.add(k, v[1]); + s.add(k, v[2]); + + if (s.totalValues(k) == 2) { + assert(keccak256(s.valueAt(k, 0)) == keccak256(v[1])); + assert(keccak256(s.valueAt(k, 1)) == keccak256(v[2])); + } else { + assert(s.totalValues(k) == 1); + + assert(keccak256(s.valueAt(k, 0)) == keccak256(v[1])); + assert(keccak256(s.valueAt(k, 0)) == keccak256(v[2])); + + assert(keccak256(v[1]) == keccak256(v[2])); + } + } +} diff --git a/tests/test/Store.t.sol b/tests/regression/test/Store.t.sol similarity index 81% rename from tests/test/Store.t.sol rename to tests/regression/test/Store.t.sol index 7d896933..db80ac6b 100644 --- a/tests/test/Store.t.sol +++ b/tests/regression/test/Store.t.sol @@ -17,30 +17,30 @@ contract StoreTest is Test { } // TODO: support symbolic base slot -// function testStore(bytes32 key, bytes32 value) public { +// function check_store(bytes32 key, bytes32 value) public { // vm.store(address(c), key, value); // assert(vm.load(address(c), key) == value); // } // TODO: support uninitialized accounts -// function testStoreUninit(bytes32 value) public { +// function check_store_Uninit(bytes32 value) public { // vm.store(address(0), 0, value); // assert(vm.load(address(0), 0) == value); // } - function testStoreScalar(uint value) public { + function check_store_Scalar(uint value) public { vm.store(address(c), 0, bytes32(value)); assert(c.x() == value); assert(uint(vm.load(address(c), 0)) == value); } - function testStoreMapping(uint key, uint value) public { + function check_store_Mapping(uint key, uint value) public { vm.store(address(c), keccak256(abi.encode(key, 1)), bytes32(value)); assert(c.m(key) == value); assert(uint(vm.load(address(c), keccak256(abi.encode(key, 1)))) == value); } - function testStoreArray(uint key, uint value) public { + function check_store_Array(uint key, uint value) public { vm.assume(key < 2**32); // to avoid overflow vm.store(address(c), bytes32(uint(2)), bytes32(uint(1) + key)); vm.store(address(c), bytes32(uint(keccak256(abi.encode(2))) + key), bytes32(value)); diff --git a/tests/regression/test/Struct.t.sol b/tests/regression/test/Struct.t.sol new file mode 100644 index 00000000..d2cdef42 --- /dev/null +++ b/tests/regression/test/Struct.t.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +/// @custom:halmos --solver-timeout-assertion 0 +contract StructTest { + struct Point { + uint x; + uint y; + } + + function check_Struct(Point memory p) public pure returns (uint result) { + unchecked { + result += p.x + p.y; + } + assert(result == 0); // expected to fail and generate a counterexample that incorporates all calldata symbols + } + + /// @custom:halmos --array-lengths p=1 + function check_StructArray(Point[] memory p, Point[2] memory q) public pure returns (uint result) { + for (uint i = 0; i < p.length; i++) { + unchecked { + result += p[i].x + p[i].y; + } + } + for (uint i = 0; i < q.length; i++) { + unchecked { + result += q[i].x + q[i].y; + } + } + assert(result == 0); // expected to fail and generate a counterexample that incorporates all calldata symbols + } + + /// @custom:halmos --array-lengths p=1,p[0]=1 + function check_StructArrayArray( + Point[][] memory p, + Point[2][] memory q, + Point[][2] memory r, + Point[2][2] memory s + ) public pure returns (uint result) { + for (uint i = 0; i < p.length; i++) { + for (uint j = 0; j < p[i].length; j++) { + unchecked { + result += p[i][j].x + p[i][j].y; + } + } + } + for (uint i = 0; i < q.length; i++) { + for (uint j = 0; j < q[i].length; j++) { + unchecked { + result += q[i][j].x + q[i][j].y; + } + } + } + for (uint i = 0; i < r.length; i++) { + for (uint j = 0; j < r[i].length; j++) { + unchecked { + result += r[i][j].x + r[i][j].y; + } + } + } + for (uint i = 0; i < s.length; i++) { + for (uint j = 0; j < s[i].length; j++) { + unchecked { + result += s[i][j].x + s[i][j].y; + } + } + } + assert(result == 0); // expected to fail and generate a counterexample that incorporates all calldata symbols + } +} + +/// @custom:halmos --solver-timeout-assertion 0 +contract StructTest2 { + struct P { + uint x; + uint[] y; + uint z; + } + + struct S { + uint f1; + P f2; + uint[] f3; + P[] f4; + uint[1] f5; + P[][] f6; + } + + /// @custom:halmos --array-lengths s=1 + function check_S(P memory p, S[] memory s) public pure returns (uint result) { + unchecked { + result += sum_P(p); + for (uint i = 0; i < s.length; i++) { + result += sum_S(s[i]); + } + } + assert(result == 0); // expected to fail and generate a counterexample that incorporates all calldata symbols + } + + function sum_P(P memory p) internal pure returns (uint result) { + unchecked { + result += p.x; + for (uint i = 0; i < p.y.length; i++) { + result += p.y[i]; + } + result += p.z; + } + } + + function sum_S(S memory s) internal pure returns (uint result) { + unchecked { + result += s.f1; + result += sum_P(s.f2); + for (uint i = 0; i < s.f3.length; i++) { + result += s.f3[i]; + } + for (uint i = 0; i < s.f4.length; i++) { + result += sum_P(s.f4[i]); + } + result += s.f5[0]; + for (uint i = 0; i < s.f6.length; i++) { + for (uint j = 0; j < s.f6[i].length; j++) { + result += sum_P(s.f6[i][j]); + } + } + } + } +} diff --git a/tests/regression/test/TestConstructor.t.sol b/tests/regression/test/TestConstructor.t.sol new file mode 100644 index 00000000..4641a597 --- /dev/null +++ b/tests/regression/test/TestConstructor.t.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +contract TestConstructorTest { + uint initialized = 1; + uint constant const = 2; + uint immutable flag; + uint value; + uint codesize_; + uint extcodesize_; + + constructor () { + flag = 3; + value = 4; + + assembly { + sstore(codesize_.slot, codesize()) + sstore(extcodesize_.slot, extcodesize(address())) + } + } + + function check_value() public view { + assert(initialized == 1); + assert(const == 2); + assert(flag == 3); + assert(value == 4); + + assert(codesize_ > 0); + assert(extcodesize_ == 0); + } +} diff --git a/tests/regression/test/Token.t.sol b/tests/regression/test/Token.t.sol new file mode 100644 index 00000000..8218e15f --- /dev/null +++ b/tests/regression/test/Token.t.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {SymTest} from "halmos-cheatcodes/SymTest.sol"; +import {Test} from "forge-std/Test.sol"; + +/// @custom:halmos --solver-timeout-assertion 0 +contract TokenTest is SymTest, Test { + Token token; + + function setUp() public { + token = new Token(); + + // set the balances of three arbitrary accounts to arbitrary symbolic values + for (uint i = 0; i < 3; i++) { + address receiver = svm.createAddress('receiver'); + uint256 amount = svm.createUint256('amount'); + token.transfer(receiver, amount); + } + } + + function check_BalanceInvariant() public { + // consider two arbitrary distinct accounts + address caller = svm.createAddress('caller'); + address others = svm.createAddress('others'); + vm.assume(others != caller); + + // record their current balances + uint256 oldBalanceCaller = token.balanceOf(caller); + uint256 oldBalanceOthers = token.balanceOf(others); + + // consider an arbitrary function call to the token from the caller + vm.prank(caller); + bytes memory data = svm.createBytes(100, 'data'); + (bool success,) = address(token).call(data); + vm.assume(success); + + // ensure that the caller cannot spend others' tokens + assert(token.balanceOf(caller) <= oldBalanceCaller); + assert(token.balanceOf(others) >= oldBalanceOthers); + } +} + +contract Token { + mapping(address => uint) public balanceOf; + + constructor() { + balanceOf[msg.sender] = 1e27; + } + + function transfer(address to, uint amount) public { + _transfer(msg.sender, to, amount); + } + + function _transfer(address from, address to, uint amount) public { + balanceOf[from] -= amount; + balanceOf[to] += amount; + } +} diff --git a/tests/regression/test/UnknownCall.t.sol b/tests/regression/test/UnknownCall.t.sol new file mode 100644 index 00000000..6a7d5ec9 --- /dev/null +++ b/tests/regression/test/UnknownCall.t.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import "forge-std/Test.sol"; + +import {IERC721TokenReceiver} from "forge-std/interfaces/IERC721.sol"; + +contract UnknownCallTest is Test { + /// @custom:halmos --uninterpreted-unknown-calls= + function check_unknown_not_allowed(address addr) public { + // empty --uninterpreted-unknown-calls + IERC721TokenReceiver(addr).onERC721Received(address(0), address(0), 0, ""); // expected to fail + } + + function check_unknown_common_callbacks(address addr) public { + // onERC721Received is included in the default --uninterpreted-unknown-calls + IERC721TokenReceiver(addr).onERC721Received(address(0), address(0), 0, ""); + } + + function check_unknown_retsize_default(address addr) public { + (bool success, bytes memory retdata) = addr.call(abi.encodeWithSelector(IERC721TokenReceiver.onERC721Received.selector, address(0), address(0), 0, "")); + assert(retdata.length == 32); // default --return-size-of-unknown-calls=32 + } + + /// @custom:halmos --return-size-of-unknown-calls=64 + function check_unknown_retsize_64(address addr) public { + (bool success, bytes memory retdata) = addr.call(abi.encodeWithSelector(IERC721TokenReceiver.onERC721Received.selector, address(0), address(0), 0, "")); + assert(retdata.length == 64); + } + + /// @custom:halmos --return-size-of-unknown-calls=0 + function check_unknown_retsize_0(address addr) public { + (bool success, bytes memory retdata) = addr.call(abi.encodeWithSelector(IERC721TokenReceiver.onERC721Received.selector, address(0), address(0), 0, "")); + assert(retdata.length == 0); + } + + function check_unknown_call(address addr, uint amount, uint initial) public { + vm.assume(addr != address(this)); + vm.deal(addr, 0); + vm.deal(address(this), initial); + + (bool success, bytes memory retdata) = payable(addr).call{ value: amount }(""); + + assert(retdata.length == 32); // default --return-size-of-unknown-calls=32 + + if (success) { + assert(initial >= amount); + assert(addr.balance == amount); + assert(address(this).balance == initial - amount); + } else { + assert(addr.balance == 0); + assert(address(this).balance == initial); + } + } + + function check_unknown_send(address addr, uint amount, uint initial) public { + vm.assume(addr != address(this)); + vm.deal(addr, 0); + vm.deal(address(this), initial); + + bool success = payable(addr).send(amount); + + if (success) { + assert(initial >= amount); + assert(addr.balance == amount); + assert(address(this).balance == initial - amount); + } else { + assert(addr.balance == 0); + assert(address(this).balance == initial); + } + } + + function check_unknown_send_fail(address addr) public { + vm.assume(addr != address(this)); + vm.deal(addr, 0); + vm.deal(address(this), 1); + + bool success = payable(addr).send(2); // get stuck + + assert(!success); + } + + function check_unknown_transfer(address addr, uint amount, uint initial) public { + vm.assume(addr != address(this)); + vm.deal(addr, 0); + vm.deal(address(this), initial); + + payable(addr).transfer(amount); // revert if fail + + // at this point, transfer succeeds because it reverts on failure + assert(initial >= amount); + assert(addr.balance == amount); + assert(address(this).balance == initial - amount); + } +} diff --git a/tests/regression/test/UnsupportedOpcode.t.sol b/tests/regression/test/UnsupportedOpcode.t.sol new file mode 100644 index 00000000..b3981571 --- /dev/null +++ b/tests/regression/test/UnsupportedOpcode.t.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +// no hop +contract X { + function foo() internal { + assembly { + selfdestruct(0) // unsupported opcode + } + } + + function check_unsupported_opcode() public { + foo(); // unsupported error + } +} + +// 1 hop +contract Y { + X x; + + function setUp() public { + x = new X(); + } + + function check_unsupported_opcode() public { + x.check_unsupported_opcode(); // unsupported error + } +} + +// 2 hops +contract Z { + Y y; + + function setUp() public { + y = new Y(); + y.setUp(); + } + + function check_unsupported_opcode() public { + y.check_unsupported_opcode(); // unsupported error + } +} diff --git a/tests/test/Warp.t.sol b/tests/regression/test/Warp.t.sol similarity index 73% rename from tests/test/Warp.t.sol rename to tests/regression/test/Warp.t.sol index da208365..d5587668 100644 --- a/tests/test/Warp.t.sol +++ b/tests/regression/test/Warp.t.sol @@ -28,39 +28,39 @@ contract WarpTest is Test { vm.warp(time); } - function testWarp(uint time) public { + function check_warp(uint time) public { vm.warp(time); assert(block.timestamp == time); } - function testWarpInternal(uint time) public { + function check_warp_Internal(uint time) public { warp(time); assert(block.timestamp == time); } - function testWarpExternal(uint time) public { + function check_warp_External(uint time) public { ext.warp(time); assert(block.timestamp == time); } - function testWarpExternalSelf(uint time) public { + function check_warp_ExternalSelf(uint time) public { this.warp(time); assert(block.timestamp == time); } - function testWarpNew(uint time) public { + function check_warp_New(uint time) public { c = new C(time); assert(block.timestamp == time); } - function testWarpReset(uint time1, uint time2) public { + function check_warp_Reset(uint time1, uint time2) public { vm.warp(time1); assert(block.timestamp == time1); vm.warp(time2); assert(block.timestamp == time2); } - function testWarpSetUp() public { + function check_warp_SetUp() public view { assert(block.timestamp == 1000); } } diff --git a/tests/solver/foundry.toml b/tests/solver/foundry.toml new file mode 100644 index 00000000..3209ea22 --- /dev/null +++ b/tests/solver/foundry.toml @@ -0,0 +1,11 @@ +[profile.default] +src = 'src' +out = 'out' +libs = ['../lib', 'lib'] + +# See more config options https://github.com/foundry-rs/foundry/tree/master/config +force = false +evm_version = 'shanghai' + +# compile options used by halmos (to prevent unnecessary recompilation when running forge test and halmos together) +extra_output = ["storageLayout", "metadata"] diff --git a/tests/solver/remappings.txt b/tests/solver/remappings.txt new file mode 100644 index 00000000..41f9d750 --- /dev/null +++ b/tests/solver/remappings.txt @@ -0,0 +1,2 @@ +openzeppelin/=../lib/openzeppelin-contracts/contracts/ +ds-test/=../lib/forge-std/lib/ds-test/src/ diff --git a/tests/solver/test/Math.t.sol b/tests/solver/test/Math.t.sol new file mode 100644 index 00000000..a56f3255 --- /dev/null +++ b/tests/solver/test/Math.t.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +contract MathTest { + function check_Avg(uint a, uint b) public pure { + unchecked { + uint r1 = (a & b) + (a ^ b) / 2; + uint r2 = (a + b) / 2; + assert(r1 == r2); + } + } + + /// @custom:halmos --solver-timeout-assertion 10000 + function check_deposit(uint a, uint A1, uint S1) public pure { + uint s = (a * S1) / A1; + + uint A2 = A1 + a; + uint S2 = S1 + s; + + // (A1 / S1 <= A2 / S2) + assert(A1 * S2 <= A2 * S1); // no counterexample + } + + /// @custom:halmos --solver-timeout-assertion 0 + function check_mint(uint s, uint A1, uint S1) public pure { + uint a = (s * A1) / S1; + + uint A2 = A1 + a; + uint S2 = S1 + s; + + // (A1 / S1 <= A2 / S2) + assert(A1 * S2 <= A2 * S1); // counterexamples exist + } +} diff --git a/tests/solver/test/SignedDiv.t.sol b/tests/solver/test/SignedDiv.t.sol new file mode 100644 index 00000000..a8064bdb --- /dev/null +++ b/tests/solver/test/SignedDiv.t.sol @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import "forge-std/Test.sol"; + +/// signed wadMul edge case in solmate: +/// https://twitter.com/transmissions11/status/1688601302371389440 +/// https://twitter.com/milotruck/status/1691136777749512192 +/// https://twitter.com/Montyly/status/1688603604062482433 + +interface WadMul { + function wadMul(int256 x, int256 y) external pure returns (int256); +} + +contract SolmateBadWadMul is WadMul { + // https://github.com/transmissions11/solmate/blob/main/src/utils/SignedWadMath.sol + // after the fix (fadb2e2778adbf01c80275bfb99e5c14969d964b) + function wadMul(int256 x, int256 y) public pure override returns (int256 r) { + /// @solidity memory-safe-assembly + assembly { + // Store x * y in r for now. + r := mul(x, y) + + // Equivalent to require(x == 0 || (x * y) / x == y) + if iszero(or(iszero(x), eq(sdiv(r, x), y))) { revert(0, 0) } + + // Scale the result down by 1e18. + r := sdiv(r, 1000000000000000000) + } + } +} + +contract SolmateGoodWadMul is WadMul { + // https://github.com/transmissions11/solmate/blob/main/src/utils/SignedWadMath.sol + // after the fix (fadb2e2778adbf01c80275bfb99e5c14969d964b) + function wadMul(int256 x, int256 y) public pure override returns (int256 r) { + /// @solidity memory-safe-assembly + assembly { + // Store x * y in r for now. + r := mul(x, y) + + // Combined overflow check (`x == 0 || (x * y) / x == y`) and edge case check + // where x == -1 and y == type(int256).min, for y == -1 and x == min int256, + // the second overflow check will catch this. + // See: https://secure-contracts.com/learn_evm/arithmetic-checks.html#arithmetic-checks-for-int256-multiplication + // Combining into 1 expression saves gas as resulting bytecode will only have 1 `JUMPI` + // rather than 2. + if iszero( + and( + or(iszero(x), eq(sdiv(r, x), y)), + or(lt(x, not(0)), sgt(y, 0x8000000000000000000000000000000000000000000000000000000000000000)) + ) + ) { revert(0, 0) } + + // Scale the result down by 1e18. + r := sdiv(r, 1000000000000000000) + } + } +} + +contract SolidityWadMul is WadMul { + function wadMul(int256 x, int256 y) public pure override returns (int256) { + return (x * y) / 1e18; + } +} + +abstract contract TestMulWad is Test { + WadMul wadMul; + SolidityWadMul solidityWadMul = new SolidityWadMul(); + + function setUp() external { + wadMul = createWadMul(); + } + + function createWadMul() internal virtual returns (WadMul); + + function check_wadMul_solEquivalent(int256 x, int256 y) external { + bytes memory encodedCall = abi.encodeWithSelector(WadMul.wadMul.selector, x, y); + + (bool succ1, bytes memory retbytes1) = address(solidityWadMul).call(encodedCall); + (bool succ2, bytes memory retbytes2) = address(wadMul).call(encodedCall); + + // if one reverts, the other should too + assertEq(succ1, succ2); + + if (succ1 && succ2) { + // if both succeed, they should return the same value + int256 result1 = abi.decode(retbytes1, (int256)); + int256 result2 = abi.decode(retbytes2, (int256)); + assertEq(result1, result2); + } + } +} + +contract TestBadWadMul is TestMulWad { + /// @dev there is an edge case, so we expect this to fail with: + // Counterexample: + // p_x_int256 = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff + // p_y_int256 = 0x8000000000000000000000000000000000000000000000000000000000000000 + function createWadMul() internal override returns (WadMul) { + return new SolmateBadWadMul(); + } +} + +contract TestGoodWadMul is TestMulWad { + function createWadMul() internal override returns (WadMul) { + return new SolmateGoodWadMul(); + } +} diff --git a/tests/solver/test/Solver.t.sol b/tests/solver/test/Solver.t.sol new file mode 100644 index 00000000..df3fdce6 --- /dev/null +++ b/tests/solver/test/Solver.t.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +// from https://github.com/a16z/halmos/issues/57 + +/// @custom:halmos --solver-timeout-assertion 10000 +contract SolverTest { + + function foo(uint x) public pure returns (uint) { + if(x < type(uint128).max) + return x * 42; + else return x; + } + + function check_foo(uint a, uint b) public pure { + if(b > a) { + assert(foo(b) > foo(a)); + } + } + +} diff --git a/tests/test/Byte.t.sol b/tests/test/Byte.t.sol deleted file mode 100644 index 6557cab5..00000000 --- a/tests/test/Byte.t.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity >=0.8.0 <0.9.0; - -contract ByteTest { - function byte1(uint i, uint x) public returns (uint r) { - assembly { r := byte(i, x) } - } - - function byte2(uint i, uint x) public returns (uint) { - if (i >= 32) return 0; - return (x >> (248-i*8)) & 0xff; - } - - function byte3(uint i, uint x) public returns (uint) { - if (i >= 32) return 0; - bytes memory b = new bytes(32); - assembly { mstore(add(b, 32), x) } - return uint(uint8(bytes1(b[i]))); // TODO: Not supported: MLOAD symbolic memory offset: 160 + p_i_uint256 - } - - function testByte(uint i, uint x) public { - uint r1 = byte1(i, x); - uint r2 = byte2(i, x); - // uint r3 = byte3(i, x); - assert(r1 == r2); - // assert(r1 == r3); - } -} diff --git a/tests/test/Console.t.sol b/tests/test/Console.t.sol deleted file mode 100644 index 2c6cc662..00000000 --- a/tests/test/Console.t.sol +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity >=0.8.0 <0.9.0; - -import "forge-std/Test.sol"; - -contract ConsoleTest is Test { - function testLog() public { - console.log(0); - console.log(1); - } -} diff --git a/tests/test/HalmosCheatCode.t.sol b/tests/test/HalmosCheatCode.t.sol deleted file mode 100644 index 41993945..00000000 --- a/tests/test/HalmosCheatCode.t.sol +++ /dev/null @@ -1,80 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity >=0.8.0 <0.9.0; - -interface SVM { - // Create a new symbolic uint value ranging over [0, 2**bitSize - 1] (inclusive) - function createUint(uint256 bitSize, string memory name) external returns (uint256 value); - - // Create a new symbolic byte array with the given byte size - function createBytes(uint256 byteSize, string memory name) external returns (bytes memory value); - - // Create a new symbolic uint256 value - function createUint256(string memory name) external returns (uint256 value); - - // Create a new symbolic bytes32 value - function createBytes32(string memory name) external returns (bytes32 value); - - // Create a new symbolic address value - function createAddress(string memory name) external returns (address value); - - // Create a new symbolic boolean value - function createBool(string memory name) external returns (bool value); -} - -abstract contract SymTest { - // SVM cheat code address: 0xf3993a62377bcd56ae39d773740a5390411e8bc9 - address internal constant SVM_ADDRESS = address(uint160(uint256(keccak256("svm cheat code")))); - - SVM internal constant svm = SVM(SVM_ADDRESS); -} - -contract HalmosCheatCodeTest is SymTest { - function testSymbolicUint() public { - uint x = svm.createUint(256, 'x'); - uint y = svm.createUint(160, 'y'); - uint z = svm.createUint(8, 'z'); - assert(0 <= x && x <= type(uint256).max); - assert(0 <= y && y <= type(uint160).max); - assert(0 <= z && z <= type(uint8).max); - } - - function testSymbolicBytes() public { - bytes memory data = svm.createBytes(2, 'data'); - uint x = uint(uint8(data[0])); - uint y = uint(uint8(data[1])); - assert(0 <= x && x <= type(uint8).max); - assert(0 <= y && y <= type(uint8).max); - } - - function testSymbolicUint256() public { - uint x = svm.createUint256('x'); - assert(0 <= x && x <= type(uint256).max); - } - - function testSymbolicBytes32() public { - bytes32 x = svm.createBytes32('x'); - assert(0 <= uint(x) && uint(x) <= type(uint256).max); - uint y; assembly { y := x } - assert(0 <= y && y <= type(uint256).max); - } - - function testSymbolicAddress() public { - address x = svm.createAddress('x'); - uint y; assembly { y := x } - assert(0 <= y && y <= type(uint160).max); - } - - function testSymbolicBool() public { - bool x = svm.createBool('x'); - uint y; assembly { y := x } - assert(y == 0 || y == 1); - } - - function testSymbolLabel() public returns (uint256) { - uint x = svm.createUint256(''); - uint y = svm.createUint256(' '); - uint z = svm.createUint256(' a '); - uint w = svm.createUint256(' a b '); - return x + y + z + w; - } -} diff --git a/tests/test/Send.t.sol b/tests/test/Send.t.sol deleted file mode 100644 index 71f0e437..00000000 --- a/tests/test/Send.t.sol +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity >=0.8.0 <0.9.0; - -contract SendTest { - - function testSend(address payable receiver, uint amount, address others) public { - require(others != address(this) && others != receiver); - - require(address(this) != receiver); - - uint oldBalanceSender = address(this).balance; - uint oldBalanceReceiver = receiver.balance; - uint oldBalanceOthers = others.balance; - - receiver.transfer(amount); - - uint newBalanceSender = address(this).balance; - uint newBalanceReceiver = receiver.balance; - uint newBalanceOthers = others.balance; - - unchecked { - assert(newBalanceSender == oldBalanceSender - amount); - assert(newBalanceReceiver == oldBalanceReceiver + amount); - assert(oldBalanceSender + oldBalanceReceiver == newBalanceSender + newBalanceReceiver); - assert(oldBalanceOthers == newBalanceOthers); - } - } - - function testSendSelf(address payable receiver, uint amount, address others) public { - require(others != address(this) && others != receiver); - - require(address(this) == receiver); - - uint oldBalanceSender = address(this).balance; - uint oldBalanceReceiver = receiver.balance; - uint oldBalanceOthers = others.balance; - - receiver.transfer(amount); - - uint newBalanceSender = address(this).balance; - uint newBalanceReceiver = receiver.balance; - uint newBalanceOthers = others.balance; - - unchecked { - assert(newBalanceSender == oldBalanceSender); - assert(newBalanceReceiver == oldBalanceReceiver); - assert(oldBalanceSender + oldBalanceReceiver == newBalanceSender + newBalanceReceiver); - assert(oldBalanceOthers == newBalanceOthers); - } - } -} diff --git a/tests/test/Struct.t.sol b/tests/test/Struct.t.sol deleted file mode 100644 index 5f1c6fbf..00000000 --- a/tests/test/Struct.t.sol +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity >=0.8.0 <0.9.0; - -contract StructTest { - struct Point { - uint x; - uint y; - } - - /* TODO: support struct parameter - function testStruct(Point memory p) public { - assert(true); - } - */ -} diff --git a/tests/test_cli.py b/tests/test_cli.py index 4ad8c674..35389abb 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -8,7 +8,6 @@ from halmos.sevm import con, Contract, Instruction from halmos.__main__ import str_abi, run_bytecode, FunctionInfo -import halmos.__main__ from test_fixtures import args, options @@ -114,20 +113,11 @@ def test_run_bytecode(args): args.symbolic_jump = True hexcode = "34381856FDFDFDFDFDFD5B00" - exs = run_bytecode(hexcode) + exs = run_bytecode(hexcode, args) assert len(exs) == 1 assert exs[0].current_opcode() == EVM.STOP -def test_setup(setup_abi, setup_name, setup_sig, setup_selector, args): - hexcode = "600100" - abi = setup_abi - setup_ex = halmos.__main__.setup( - hexcode, abi, FunctionInfo(setup_name, setup_sig, setup_selector), args - ) - assert setup_ex.st.stack == [1] - - def test_instruction(): assert str(Instruction(con(0))) == "STOP" assert str(Instruction(con(1))) == "ADD" @@ -362,6 +352,16 @@ def test_decode(): """, ), ], + ids=( + "fooInt(uint256)", + "fooInt8(uint8)", + "fooIntAddress(uint256,address)", + "fooIntInt(uint256,uint256)", + "fooStruct(((uint256,uint256),uint256),uint256)", + "fooDynArr(uint256[])", + "fooFixArr(uint256[3])", + "fooBytes(bytes)", + ), ) def test_str_abi(sig, abi): assert sig == str_abi(json.loads(abi)) diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index b5948437..c2c745f4 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -2,13 +2,13 @@ from halmos.sevm import SEVM -from halmos.__main__ import parse_args, mk_options +from halmos.__main__ import arg_parser, mk_options, mk_solver import halmos.__main__ @pytest.fixture def args(): - args = parse_args([]) + args = arg_parser.parse_args([]) # set the global args for the main module halmos.__main__.args = args @@ -24,3 +24,13 @@ def options(args): @pytest.fixture def sevm(options): return SEVM(options) + + +@pytest.fixture +def solver(args): + return mk_solver(args) + + +@pytest.fixture +def halmos_options(request): + return request.config.getoption("--halmos-options") diff --git a/tests/test_halmos.py b/tests/test_halmos.py new file mode 100644 index 00000000..c99261ea --- /dev/null +++ b/tests/test_halmos.py @@ -0,0 +1,99 @@ +import pytest +import json + +from typing import Dict +from dataclasses import asdict + +from halmos.__main__ import _main, rendered_calldata +from halmos.sevm import con + +from test_fixtures import halmos_options + + +@pytest.mark.parametrize( + "cmd, expected_path", + [ + ( + ["--root", "tests/regression"], + "tests/expected/all.json", + ), + ( + ["--root", "tests/ffi"], + "tests/expected/ffi.json", + ), + ( + ["--root", "tests/solver"], + "tests/expected/solver.json", + ), + ( + ["--root", "examples/simple"], + "tests/expected/simple.json", + ), + ( + ["--root", "examples/tokens/ERC20"], + "tests/expected/erc20.json", + ), + ( + ["--root", "examples/tokens/ERC721"], + "tests/expected/erc721.json", + ), + ], + ids=( + "tests/regression", + "tests/ffi", + "long:tests/solver", + "long:examples/simple", + "long:examples/tokens/ERC20", + "long:examples/tokens/ERC721", + ), +) +def test_main(cmd, expected_path, halmos_options): + actual = asdict(_main(cmd + halmos_options.split())) + with open(expected_path, encoding="utf8") as f: + expected = json.load(f) + assert expected["exitcode"] == actual["exitcode"] + assert_eq(expected["test_results"], actual["test_results"]) + + +@pytest.mark.parametrize( + "cmd", + [ + ["--root", "tests/regression", "--contract", "SetupFailTest"], + ], + ids=("SetupFailTest",), +) +def test_main_fail(cmd, halmos_options): + actual = asdict(_main(cmd + halmos_options.split())) + assert actual["exitcode"] != 0 + + +def assert_eq(m1: Dict, m2: Dict) -> int: + assert list(m1.keys()) == list(m2.keys()) + for c in m1: + l1 = sorted(m1[c], key=lambda x: x["name"]) + l2 = sorted(m2[c], key=lambda x: x["name"]) + assert len(l1) == len(l2), c + for r1, r2 in zip(l1, l2): + assert r1["name"] == r2["name"] + assert r1["exitcode"] == r2["exitcode"], f"{c} {r1['name']}" + assert r1["num_models"] == r2["num_models"], f"{c} {r1['name']}" + + +def test_rendered_calldata_symbolic(): + assert rendered_calldata([con(1, 8), con(2, 8), con(3, 8)]) == "0x010203" + + +def test_rendered_calldata_symbolic_singleton(): + assert rendered_calldata([con(0x42, 8)]) == "0x42" + + +def test_rendered_calldata_concrete(): + assert rendered_calldata([1, 2, 3]) == "0x010203" + + +def test_rendered_calldata_mixed(): + assert rendered_calldata([con(1, 8), 2, con(3, 8)]) == "0x010203" + + +def test_rendered_calldata_empty(): + assert rendered_calldata([]) == "0x" diff --git a/tests/test_sevm.py b/tests/test_sevm.py index f85d44a6..0836be59 100644 --- a/tests/test_sevm.py +++ b/tests/test_sevm.py @@ -2,6 +2,7 @@ from z3 import * +from halmos.exceptions import OutOfGasError from halmos.utils import EVM from halmos.sevm import ( @@ -13,6 +14,8 @@ f_smod, f_exp, f_origin, + CallContext, + Message, SEVM, Exec, int_of, @@ -21,11 +24,13 @@ iter_bytes, wload, wstore, + wstore_bytes, + Path, ) from halmos.__main__ import mk_block -from test_fixtures import args, options, sevm +from test_fixtures import args, options, sevm, solver caller = BitVec("msg_sender", 160) @@ -36,32 +41,24 @@ callvalue = BitVec("msg_value", 256) -@pytest.fixture -def solver(args): - solver = SolverFor( - "QF_AUFBV" - ) # quantifier-free bitvector + array theory; https://smtlib.cs.uiowa.edu/logics.shtml - solver.set(timeout=args.solver_timeout_branching) - return solver - - @pytest.fixture def storage(): return {} def mk_ex(hexcode, sevm, solver, storage, caller, this): + bytecode = Contract(hexcode) + message = Message(target=this, caller=caller, value=callvalue, data=[]) return sevm.mk_exec( - code={this: Contract(hexcode)}, + code={this: bytecode}, storage={this: storage}, balance=balance, block=mk_block(), - calldata=[], - callvalue=callvalue, - caller=caller, + context=CallContext(message), this=this, + pgm=bytecode, symbolic=True, - solver=solver, + path=Path(solver), ) @@ -77,22 +74,22 @@ def o(opcode): @pytest.mark.parametrize( "hexcode, stack, pc, opcode", [ - (BitVecVal(int("600100", 16), 24), "[1]", 2, EVM.STOP), + (BitVecVal(0x600100, 24), "[1]", 2, EVM.STOP), # symbolic opcodes are not supported # (BitVec('x', 256), '[]', 0, 'Extract(255, 248, x)'), # (Concat(BitVecVal(int('6001', 16), 16), BitVec('x', 8), BitVecVal(0, 8)), '[1]', 2, 'x'), ( - Concat(BitVecVal(int("6101", 16), 16), BitVec("x", 8), BitVecVal(0, 8)), + Concat(BitVecVal(0x6101, 16), BitVec("x", 8), BitVecVal(0, 8)), "[Concat(1, x)]", 3, EVM.STOP, ), - (BitVecVal(int("58585B5860015800", 16), 64), "[6, 1, 3, 1, 0]", 7, EVM.STOP), + (BitVecVal(0x58585B5860015800, 64), "[0, 1, 3, 1, 6]", 7, EVM.STOP), ], ) def test_run(hexcode, stack, pc, opcode: int, sevm, solver, storage): ex = mk_ex(hexcode, sevm, solver, storage, caller, this) - (exs, _, _) = sevm.run(ex) + exs = list(sevm.run(ex)) assert len(exs) == 1 ex: Exec = exs[0] assert str(ex.st.stack) == stack @@ -279,11 +276,47 @@ def byte_of(i, x): ) def test_opcode_simple(hexcode, params, output, sevm: SEVM, solver, storage): ex = mk_ex(Concat(hexcode, o(EVM.STOP)), sevm, solver, storage, caller, this) - ex.st.stack.extend(params) - (exs, _, _) = sevm.run(ex) + + # reversed because in the tests the stack is written with the top on the left + # but in the internal state, the top of the stack is the last element of the list + ex.st.stack.extend(reversed(params)) + exs = list(sevm.run(ex)) + assert len(exs) == 1 + ex = exs[0] + assert ex.st.stack.pop() == simplify(output) + + +@pytest.mark.parametrize( + "hexcode, stack_in, stack_out", + [ + (o(EVM.SWAP1), [x, y, z], [y, x, z]), + (o(EVM.SWAP2), [x, y, z], [z, y, x]), + (o(EVM.SWAP3), [x, 1, 2, y, 3], [y, 1, 2, x, 3]), + (o(EVM.SWAP4), [x, 1, 2, 3, y, 4], [y, 1, 2, 3, x, 4]), + (o(EVM.SWAP5), [x, 1, 2, 3, 4, y, 5], [y, 1, 2, 3, 4, x, 5]), + (o(EVM.SWAP6), [x, 1, 2, 3, 4, 5, y, 6], [y, 1, 2, 3, 4, 5, x, 6]), + (o(EVM.SWAP7), [x, 1, 2, 3, 4, 5, 6, y, 7], [y, 1, 2, 3, 4, 5, 6, x, 7]), + (o(EVM.SWAP8), [x, 1, 2, 3, 4, 5, 6, 7, y, 8], [y, 1, 2, 3, 4, 5, 6, 7, x, 8]), + (o(EVM.SWAP9), [x] + [0] * 8 + [y, 9], [y] + [0] * 8 + [x, 9]), + (o(EVM.SWAP10), [x] + [0] * 9 + [y, 10], [y] + [0] * 9 + [x, 10]), + (o(EVM.SWAP11), [x] + [0] * 10 + [y, 11], [y] + [0] * 10 + [x, 11]), + (o(EVM.SWAP12), [x] + [0] * 11 + [y, 12], [y] + [0] * 11 + [x, 12]), + (o(EVM.SWAP13), [x] + [0] * 12 + [y, 13], [y] + [0] * 12 + [x, 13]), + (o(EVM.SWAP14), [x] + [0] * 13 + [y, 14], [y] + [0] * 13 + [x, 14]), + (o(EVM.SWAP15), [x] + [0] * 14 + [y, 15], [y] + [0] * 14 + [x, 15]), + (o(EVM.SWAP16), [x] + [0] * 15 + [y, 16], [y] + [0] * 15 + [x, 16]), + ], +) +def test_opcode_stack(hexcode, stack_in, stack_out, sevm: SEVM, solver, storage): + ex = mk_ex(Concat(hexcode, o(EVM.STOP)), sevm, solver, storage, caller, this) + + # reversed because in the tests the stack is written with the top on the left + # but in the internal state, the top of the stack is the last element of the list + ex.st.stack.extend(reversed(stack_in)) + exs = list(sevm.run(ex)) assert len(exs) == 1 ex = exs[0] - assert ex.st.stack[0] == simplify(output) + assert ex.st.stack == list(reversed(stack_out)) def test_stack_underflow_pop(sevm: SEVM, solver, storage): @@ -293,7 +326,18 @@ def test_stack_underflow_pop(sevm: SEVM, solver, storage): # TODO: from the outside, we should get an execution with failed=True # TODO: from the outside, we should get an specific exception like StackUnderflowError with pytest.raises(Exception): - sevm.run(ex) + list(sevm.run(ex)) + + +def test_large_memory_offset(sevm: SEVM, solver, storage): + # check that we get an exception when popping from an empty stack + for op in [o(EVM.MLOAD), o(EVM.MSTORE), o(EVM.MSTORE8)]: + ex = mk_ex(op, sevm, solver, storage, caller, this) + ex.st.stack.append(con(42)) # value, ignored by MLOAD + ex.st.stack.append(con(2**64)) # offset too big to fit in memory + + exs = list(sevm.run(ex)) + assert len(exs) == 1 and isinstance(exs[0].context.output.error, OutOfGasError) def test_iter_bytes_bv_val(): @@ -361,3 +405,19 @@ def test_wload_bad_byte(): with pytest.raises(ValueError): wload([512], 0, 1, prefer_concrete=False) + + +def test_wstore_bytes_concrete(): + mem = [0] * 4 + wstore_bytes(mem, 0, 4, bytes.fromhex("12345678")) + assert mem == [0x12, 0x34, 0x56, 0x78] + + +def test_wstore_bytes_concolic(): + mem1 = [0] * 4 + wstore(mem1, 0, 4, con(0x12345678, 32)) + + mem2 = [0] * 4 + wstore_bytes(mem2, 0, 4, mem1) + + assert mem2 == [0x12, 0x34, 0x56, 0x78] diff --git a/tests/test_traces.py b/tests/test_traces.py new file mode 100644 index 00000000..ebd3ea1e --- /dev/null +++ b/tests/test_traces.py @@ -0,0 +1,892 @@ +import json +import pytest +import subprocess + +from dataclasses import dataclass +from typing import Dict, List, Any, Optional + +from z3 import * + +from halmos.__main__ import mk_block, render_trace +from halmos.exceptions import * +from halmos.sevm import ( + CallContext, + CallOutput, + Contract, + EventLog, + Exec, + Message, + NotConcreteError, + SEVM, + byte_length, + con, + int_of, + wstore, +) +from halmos.utils import EVM +from test_fixtures import args, options, sevm + +import halmos.sevm + +# keccak256("FooEvent()") +FOO_EVENT_SIG = 0x34E21A9428B1B47E73C4E509EABEEA7F2B74BECA07D82AAC87D4DD28B74C2A4A + +# keccak256("Log(uint256)") +LOG_U256_SIG = 0x909C57D5C6AC08245CF2A6DE3900E2B868513FA59099B92B27D8DB823D92DF9C + +# bytes4(keccak256("Panic(uint256)")) + bytes32(1) +PANIC_1 = 0x4E487B710000000000000000000000000000000000000000000000000000000000000001 + +DEFAULT_EMPTY_CONSTRUCTOR = """ +contract Foo {} +""" + + +FAILED_CREATE = """ +contract Bar { + uint256 immutable x; + + constructor(uint256 _x) { + assert(_x != 42); + x = _x; + } +} + +contract Foo { + function go() public returns(bool success) { + bytes memory creationCode = abi.encodePacked( + type(Bar).creationCode, + uint256(42) + ); + + address addr; + bytes memory returndata; + + assembly { + addr := create(0, add(creationCode, 0x20), mload(creationCode)) + + // advance free mem pointer to allocate `size` bytes + let free_mem_ptr := mload(0x40) + mstore(0x40, add(free_mem_ptr, returndatasize())) + + returndata := free_mem_ptr + mstore(returndata, returndatasize()) + + let offset := add(returndata, 32) + returndatacopy( + offset, + 0, // returndata offset + returndatasize() + ) + } + + success = (addr != address(0)); + // assert(returndata.length == 36); + } +} +""" + + +caller = BitVec("msg_sender", 160) +this = BitVec("this_address", 160) +balance = Array("balance_0", BitVecSort(160), BitVecSort(256)) + +# 0x0f59f83a is keccak256("go()") +default_calldata = list(con(x, size_bits=8) for x in bytes.fromhex("0f59f83a")) + +go_uint256_selector = BitVecVal(0xB20E7344, 32) # keccak256("go(uint256)") +p_x_uint256 = BitVec("p_x_uint256", 256) +go_uint256_calldata: List[BitVecRef] = [] +wstore(go_uint256_calldata, 0, 4, go_uint256_selector) +wstore(go_uint256_calldata, 4, 32, p_x_uint256) + + +@pytest.fixture +def solver(args): + solver = SolverFor( + "QF_AUFBV" + ) # quantifier-free bitvector + array theory; https://smtlib.cs.uiowa.edu/logics.shtml + solver.set(timeout=args.solver_timeout_branching) + return solver + + +@pytest.fixture +def storage(): + return {} + + +@dataclass(frozen=True) +class SingleResult: + is_single: bool + value: Optional[Any] + + # allows tuple-like unpacking + def __iter__(self): + return iter((self.is_single, self.value)) + + +NO_SINGLE_RESULT = SingleResult(False, None) + + +def single(iterable) -> SingleResult: + """ + Returns (True, element) if the iterable has exactly one element + or (False, None) otherwise. + + Note: + - if the iterable has a single None element, this returns (True, None) + """ + iterator = iter(iterable) + element = None + try: + element = next(iterator) + except StopIteration: + return NO_SINGLE_RESULT + + try: + next(iterator) + return NO_SINGLE_RESULT + except StopIteration: + return SingleResult(True, element) + + +def is_single(iterable) -> bool: + """ + Returns True if the iterable has exactly one element, False otherwise. + """ + + return single(iterable).is_single + + +def empty(iterable) -> bool: + """ + Returns True if the iterable is empty, False otherwise. + """ + iterator = iter(iterable) + try: + next(iterator) + return False + except StopIteration: + return True + + +def render_path(ex: Exec) -> None: + path = list(dict.fromkeys(ex.path)) + path.remove("True") + print(f"Path: {', '.join(path)}") + + +def mk_create_ex( + hexcode, sevm, solver, caller=caller, value=con(0), this=this, storage={} +) -> Exec: + bytecode = Contract(hexcode) + storage[this] = {} + + message = Message( + target=this, + caller=caller, + value=value, + data=[], + is_static=False, + ) + + return sevm.mk_exec( + code={}, + storage=storage, + balance=balance, + block=mk_block(), + context=CallContext(message=message), + this=this, + pgm=bytecode, + symbolic=True, + solver=solver, + ) + + +def mk_ex( + hexcode, + sevm, + solver, + caller=caller, + value=con(0), + this=this, + storage={}, + data=default_calldata, +) -> Exec: + bytecode = Contract(hexcode) + storage[this] = {} + + message = Message( + target=this, + caller=caller, + value=value, + data=data, + is_static=False, + ) + + return sevm.mk_exec( + code={this: bytecode}, + storage=storage, + balance=balance, + block=mk_block(), + context=CallContext(message=message), + this=this, + pgm=bytecode, + symbolic=True, + solver=solver, + ) + + +BuildOutput = Dict + + +def compile(source: str) -> BuildOutput: + proc = subprocess.Popen( + ["solc", "--combined-json", "bin,bin-runtime", "--no-cbor-metadata", "-"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + (stdout, stderr) = proc.communicate(input=source.encode("utf-8")) + if proc.returncode != 0: + raise Exception("solc failed: " + stderr.decode("utf-8")) + return json.loads(stdout) + + +def find_contract(contract_name: str, build_output: BuildOutput) -> Dict: + for name in build_output["contracts"]: + if name.endswith(f":{contract_name}"): + return build_output["contracts"][name] + + raise Exception(f"Contract {contract_name} not found in {build_output}") + + +def get_bytecode(source: str, contract_name: str = "Foo"): + build_output = compile(source) + contract_object = find_contract(contract_name, build_output) + return contract_object["bin"], contract_object["bin-runtime"] + + +def test_deploy_basic(sevm, solver): + deploy_hexcode, runtime_hexcode = get_bytecode(DEFAULT_EMPTY_CONSTRUCTOR) + exec: Exec = mk_create_ex(deploy_hexcode, sevm, solver) + + # before execution + assert exec.context.output.data is None + + sevm.run(exec) + render_trace(exec.context) + + # after execution + assert exec.context.output.error is None + assert exec.context.output.data == bytes.fromhex(runtime_hexcode) + assert len(exec.context.trace) == 0 + + +def test_deploy_nonpayable_reverts(sevm, solver): + deploy_hexcode, _ = get_bytecode(DEFAULT_EMPTY_CONSTRUCTOR) + exec: Exec = mk_create_ex(deploy_hexcode, sevm, solver, value=con(1)) + + sevm.run(exec) + render_trace(exec.context) + + assert isinstance(exec.context.output.error, Revert) + assert not exec.context.output.data + assert len(exec.context.trace) == 0 + + +def test_deploy_payable(sevm, solver): + deploy_hexcode, runtime_hexcode = get_bytecode( + """ + contract Foo { + constructor() payable {} + } + """ + ) + exec: Exec = mk_create_ex(deploy_hexcode, sevm, solver, value=con(1)) + + sevm.run(exec) + render_trace(exec.context) + + assert exec.context.output.error is None + assert exec.context.output.data == bytes.fromhex(runtime_hexcode) + assert len(exec.context.trace) == 0 + + +def test_deploy_event_in_constructor(sevm, solver): + deploy_hexcode, _ = get_bytecode( + """ + contract Foo { + event FooEvent(); + + constructor() { + emit FooEvent(); + } + } + """ + ) + exec: Exec = mk_create_ex(deploy_hexcode, sevm, solver) + + sevm.run(exec) + render_trace(exec.context) + + assert exec.context.output.error is None + assert len(exec.context.trace) == 1 + + event: EventLog = exec.context.trace[0] + assert len(event.topics) == 1 + assert int_of(event.topics[0]) == FOO_EVENT_SIG + assert not event.data + + +def test_simple_call(sevm: SEVM, solver): + _, runtime_hexcode = get_bytecode( + """ + contract Foo { + function view_func() public pure returns (uint) { + return 42; + } + + function go() public view returns (bool success) { + (success, ) = address(this).staticcall(abi.encodeWithSignature("view_func()")); + } + } + """ + ) + input_exec: Exec = mk_ex(runtime_hexcode, sevm, solver) + + execs = sevm.run(input_exec)[0] + (is_single_exec, output_exec) = single(execs) + assert is_single_exec + + render_trace(output_exec.context) + + assert output_exec.context.output is not None + assert output_exec.context.output.error is None + + # go() returns success=true + assert int_of(output_exec.context.output.data) == 1 + + # view_func() returns 42 + (is_single_call, subcall) = single(output_exec.context.subcalls()) + assert is_single_call + assert subcall.output.error is None + assert int_of(subcall.output.data) == 42 + + +def test_failed_call(sevm: SEVM, solver): + _, runtime_hexcode = get_bytecode( + """ + contract Foo { + function just_fails() public pure returns (uint) { + assert(false); + } + + function go() public view returns (bool success) { + (success, ) = address(this).staticcall(abi.encodeWithSignature("just_fails()")); + } + } + """ + ) + input_exec: Exec = mk_ex(runtime_hexcode, sevm, solver) + + execs = sevm.run(input_exec)[0] + (is_single_exec, output_exec) = single(execs) + assert is_single_exec + + render_trace(output_exec.context) + + # go() does not revert, it returns success=false + assert output_exec.context.output.error is None + assert int_of(output_exec.context.output.data) == 0 + + # the just_fails() subcall fails + (is_single_call, subcall) = single(output_exec.context.subcalls()) + assert is_single_call + assert isinstance(subcall.output.error, Revert) + assert int_of(subcall.output.data) == PANIC_1 + + +def test_failed_static_call(sevm: SEVM, solver): + _, runtime_hexcode = get_bytecode( + """ + contract Foo { + uint256 x; + + function do_sstore() public returns (uint) { + unchecked { + x += 1; + } + } + + function go() public view returns (bool success) { + (success, ) = address(this).staticcall(abi.encodeWithSignature("do_sstore()")); + } + } + """ + ) + input_exec: Exec = mk_ex(runtime_hexcode, sevm, solver) + + execs = sevm.run(input_exec)[0] + (is_single_exec, output_exec) = single(execs) + assert is_single_exec + + render_trace(output_exec.context) + + # go() does not revert, it returns success=false + assert output_exec.context.output.error is None + assert int_of(output_exec.context.output.data) == 0 + + # the do_sstore() subcall fails + (is_single_call, subcall) = single(output_exec.context.subcalls()) + assert is_single_call + assert subcall.message.is_static is True + assert isinstance(subcall.output.error, WriteInStaticContext) + + +def test_symbolic_subcall(sevm: SEVM, solver): + _, runtime_hexcode = get_bytecode( + """ + contract Foo { + function may_fail(uint256 x) public pure returns (uint) { + assert(x != 42); + } + + function go(uint256 x) public view returns (bool success) { + (success, ) = address(this).staticcall(abi.encodeWithSignature("may_fail(uint256)", x)); + } + } + """ + ) + input_exec: Exec = mk_ex(runtime_hexcode, sevm, solver, data=go_uint256_calldata) + + execs = sevm.run(input_exec)[0] + + # we get 2 executions, one for x == 42 and one for x != 42 + assert len(execs) == 2 + render_trace(execs[0].context) + render_trace(execs[1].context) + + # all executions have exactly one subcall and the outer call does not revert + assert all(is_single(x.context.subcalls()) for x in execs) + assert all(x.context.output.error is None for x in execs) + + # in one of the executions, the subcall succeeds + subcalls = list(single(x.context.subcalls()).value for x in execs) + assert any(subcall.output.error is None for subcall in subcalls) + + # in one of the executions, the subcall reverts + assert any(isinstance(subcall.output.error, Revert) for subcall in subcalls) + + +def test_symbolic_create(sevm: SEVM, solver): + _, runtime_hexcode = get_bytecode( + """ + contract Bar { + uint256 immutable x; + + constructor(uint256 _x) { + assert(_x != 42); + x = _x; + } + } + + contract Foo { + function go(uint256 x) public returns (bool success) { + try new Bar(x) { + success = true; + } catch { + success = false; + } + } + } + """ + ) + input_exec: Exec = mk_ex(runtime_hexcode, sevm, solver, data=go_uint256_calldata) + + execs = sevm.run(input_exec)[0] + + # we get 2 executions, one for x == 42 and one for x != 42 + assert len(execs) == 2 + render_trace(execs[0].context) + render_trace(execs[1].context) + + # all executions have exactly one subcall and the outer call does not revert + assert all(is_single(x.context.subcalls()) for x in execs) + assert all(x.context.output.error is None for x in execs) + + # in one of the executions, the subcall succeeds + subcalls = list(single(x.context.subcalls()).value for x in execs) + assert any(subcall.output.error is None for subcall in subcalls) + + # in one of the executions, the subcall reverts + assert any(isinstance(subcall.output.error, Revert) for subcall in subcalls) + + +def test_failed_create(sevm: SEVM, solver): + _, runtime_hexcode = get_bytecode(FAILED_CREATE) + input_exec: Exec = mk_ex(runtime_hexcode, sevm, solver) + + execs = sevm.run(input_exec)[0] + (is_single_exec, output_exec) = single(execs) + assert is_single_exec + + render_trace(output_exec.context) + + # go() does not revert, it returns success=false + assert output_exec.context.output.error is None + assert int_of(output_exec.context.output.data) == 0 + + # the create() subcall fails + (is_single_call, subcall) = single(output_exec.context.subcalls()) + assert is_single_call + assert isinstance(subcall.output.error, Revert) + assert int_of(subcall.output.data) == PANIC_1 + + +def test_event_conditional_on_symbol(sevm: SEVM, solver): + _, runtime_hexcode = get_bytecode( + """ + contract Foo { + event Log(uint256 x); + + function may_log(uint256 x) public returns (uint) { + if (x == 42) { + emit Log(x); + } + } + + function go(uint256 x) public returns (bool success) { + (success, ) = address(this).staticcall(abi.encodeWithSignature("may_log(uint256)", x)); + if (x != 42) { + emit Log(x); + } + } + } + """ + ) + input_exec: Exec = mk_ex(runtime_hexcode, sevm, solver, data=go_uint256_calldata) + + execs = sevm.run(input_exec)[0] + + for e in execs: + render_path(e) + render_trace(e.context) + + assert len(execs) == 2 + + # all executions have a single subcall + assert all(is_single(x.context.subcalls()) for x in execs) + + all_subcalls = list(single(x.context.subcalls()).value for x in execs) + + # one execution has a single subcall that reverts + assert any( + isinstance(subcall.output.error, WriteInStaticContext) + for subcall in all_subcalls + ) + + # one execution has a single subcall that succeeds and emits an event + assert any( + single(x.context.subcalls()).value.output.error is None + and is_single(x.context.logs()) + for x in execs + ) + + +def test_symbolic_event_data(sevm: SEVM, solver): + _, runtime_hexcode = get_bytecode( + """ + contract Foo { + event Log(uint256 x); + + function go(uint256 x) public returns (bool success) { + emit Log(x); + } + } + """ + ) + + input_exec: Exec = mk_ex(runtime_hexcode, sevm, solver, data=go_uint256_calldata) + execs = sevm.run(input_exec)[0] + (is_single_exec, output_exec) = single(execs) + assert is_single_exec + + (is_single_event, event) = single(output_exec.context.logs()) + assert is_single_event + assert len(event.topics) == 1 + assert int_of(event.topics[0]) == LOG_U256_SIG + assert is_bv(event.data) and event.data.decl().name() == "p_x_uint256" + + +def test_symbolic_event_topic(sevm: SEVM, solver): + _, runtime_hexcode = get_bytecode( + """ + contract Foo { + event Log(uint256 indexed x); + + function go(uint256 x) public returns (bool success) { + emit Log(x); + } + } + """ + ) + + input_exec: Exec = mk_ex(runtime_hexcode, sevm, solver, data=go_uint256_calldata) + execs = sevm.run(input_exec)[0] + (is_single_exec, output_exec) = single(execs) + assert is_single_exec + + (is_single_event, event) = single(output_exec.context.logs()) + assert is_single_event + assert len(event.topics) == 2 + assert int_of(event.topics[0]) == LOG_U256_SIG + assert is_bv(event.topics[1]) and event.topics[1].decl().name() == "p_x_uint256" + assert not event.data + + +def test_trace_ordering(sevm: SEVM, solver): + _, runtime_hexcode = get_bytecode( + """ + contract Foo { + event FooEvent(); + + function view_func1() public view returns (uint) { + return gasleft(); + } + + function view_func2() public view returns (uint) { + return 42; + } + + function go(uint256 x) public returns (bool success) { + (bool succ1, ) = address(this).staticcall(abi.encodeWithSignature("view_func1()")); + emit FooEvent(); + (bool succ2, ) = address(this).staticcall(abi.encodeWithSignature("view_func2()")); + success = succ1 && succ2; + } + } + """ + ) + + input_exec: Exec = mk_ex(runtime_hexcode, sevm, solver, data=go_uint256_calldata) + execs = sevm.run(input_exec)[0] + (is_single_exec, output_exec) = single(execs) + assert is_single_exec + + top_level_call = output_exec.context + render_trace(top_level_call) + + assert len(list(top_level_call.subcalls())) == 2 + call1, call2 = tuple(top_level_call.subcalls()) + + (is_single_event, event) = single(top_level_call.logs()) + assert is_single_event + + # the trace must preserve the ordering + assert top_level_call.trace == [call1, event, call2] + assert int_of(call2.output.data) == 42 + + +def test_static_context_propagates(sevm: SEVM, solver): + _, runtime_hexcode = get_bytecode( + """ + contract Foo { + event FooEvent(); + + function logFoo() public returns (uint) { + emit FooEvent(); + return 42; + } + + function view_func() public returns (bool succ) { + (succ, ) = address(this).call(abi.encodeWithSignature("logFoo()")); + } + + function go(uint256 x) public view { + (bool outerSucc, bytes memory ret) = address(this).staticcall(abi.encodeWithSignature("view_func()")); + assert(outerSucc); + + bool innerSucc = abi.decode(ret, (bool)); + assert(!innerSucc); + } + } + """ + ) + + input_exec: Exec = mk_ex(runtime_hexcode, sevm, solver, data=go_uint256_calldata) + + execs = sevm.run(input_exec)[0] + (is_single_exec, output_exec) = single(execs) + assert is_single_exec + + top_level_call = output_exec.context + render_trace(top_level_call) + + (is_single_call, outer_call) = single(top_level_call.subcalls()) + assert is_single_call + + assert outer_call.message.call_scheme == EVM.STATICCALL + assert outer_call.message.is_static is True + assert outer_call.output.error is None + + assert len(list(outer_call.subcalls())) == 1 + inner_call = next(outer_call.subcalls()) + + assert inner_call.message.call_scheme == EVM.CALL + assert inner_call.message.is_static is True + assert isinstance(inner_call.output.error, WriteInStaticContext) + assert next(inner_call.logs(), None) is None + + +def test_halmos_exception_halts_path(sevm: SEVM, solver): + _, runtime_hexcode = get_bytecode( + """ + contract Foo { + function sload(uint256 x) public view returns (uint256 value) { + assembly { + value := sload(x) + } + } + + function go(uint256 x) public view { + this.sload(x); + } + } + """ + ) + + input_exec: Exec = mk_ex(runtime_hexcode, sevm, solver, data=go_uint256_calldata) + + execs = sevm.run(input_exec)[0] + (is_single_exec, output_exec) = single(execs) + assert is_single_exec + + outer_call = output_exec.context + render_trace(outer_call) + + # outer call does not return because of NotConcreteError + assert outer_call.output.error is None + assert outer_call.output.return_scheme == None + + (is_single_call, inner_call) = single(outer_call.subcalls()) + assert is_single_call + + assert inner_call.message.call_scheme == EVM.STATICCALL + assert isinstance(inner_call.output.error, NotConcreteError) + assert not inner_call.output.data + assert inner_call.output.return_scheme == EVM.SLOAD + + +def test_deploy_symbolic_bytecode(sevm: SEVM, solver): + _, runtime_hexcode = get_bytecode( + """ + contract Foo { + function go(uint256 x) public { + assembly { + mstore(0, x) + let addr := create(0, 0, 32) + } + } + } + """ + ) + + input_exec: Exec = mk_ex(runtime_hexcode, sevm, solver, data=go_uint256_calldata) + + execs = sevm.run(input_exec)[0] + (is_single_exec, output_exec) = single(execs) + assert is_single_exec + + outer_call = output_exec.context + render_trace(outer_call) + + # outer call does not return because of NotConcreteError + assert outer_call.output.error is None + assert outer_call.output.return_scheme == None + + (is_single_call, inner_call) = single(outer_call.subcalls()) + assert is_single_call + assert inner_call.message.call_scheme == EVM.CREATE + + assert isinstance(inner_call.output.error, NotConcreteError) + assert not inner_call.output.data + assert is_bv(inner_call.output.return_scheme) + + +def test_deploy_empty_runtime_bytecode(sevm: SEVM, solver): + for creation_bytecode_len in (0, 1): + _, runtime_hexcode = get_bytecode( + f""" + contract Foo {{ + function go() public {{ + assembly {{ + let addr := create(0, 0, {creation_bytecode_len}) + }} + }} + }} + """ + ) + + input_exec: Exec = mk_ex(runtime_hexcode, sevm, solver, data=default_calldata) + + execs = sevm.run(input_exec)[0] + (is_single_exec, output_exec) = single(execs) + assert is_single_exec + + outer_call = output_exec.context + render_trace(outer_call) + + (is_single_call, inner_call) = single(outer_call.subcalls()) + assert is_single_call + assert inner_call.message.call_scheme == EVM.CREATE + assert len(inner_call.message.data) == creation_bytecode_len + + assert inner_call.output.error is None + assert len(inner_call.output.data) == 0 + assert inner_call.output.return_scheme == EVM.STOP + + +def test_call_limit_with_create(monkeypatch, sevm: SEVM, solver): + _, runtime_hexcode = get_bytecode( + """ + contract Foo { + function go() public { + // bytecode for: + // codecopy(0, 0, codesize()) + // create(0, 0, codesize()) + bytes memory creationCode = hex"386000803938600080f050"; + assembly { + let addr := create(0, add(creationCode, 0x20), mload(creationCode)) + } + } + } + """ + ) + + input_exec: Exec = mk_ex(runtime_hexcode, sevm, solver, data=default_calldata) + + # override the call depth limit to 3 (the test runs faster) + MAX_CALL_DEPTH_OVERRIDE = 3 + monkeypatch.setattr(halmos.sevm, "MAX_CALL_DEPTH", MAX_CALL_DEPTH_OVERRIDE) + + execs = sevm.run(input_exec)[0] + (is_single_exec, output_exec) = single(execs) + assert is_single_exec + + outer_call = output_exec.context + render_trace(outer_call) + + assert outer_call.output.error is None + assert byte_length(outer_call.output.data) == 0 + assert not outer_call.is_stuck() + + # peel the layer of the call stack onion until we get to the innermost call + inner_call = outer_call + for _ in range(MAX_CALL_DEPTH_OVERRIDE): + (is_single_call, inner_call) = single(inner_call.subcalls()) + assert is_single_call + assert inner_call.message.call_scheme == EVM.CREATE + assert byte_length(inner_call.output.data) == 0 + + assert isinstance(inner_call.output.error, MessageDepthLimitError)