From ad2528f67125521d9f48253c42ec29e8b7d8fd2d Mon Sep 17 00:00:00 2001 From: HugoDowsers <123469473+HugoDowsers@users.noreply.github.com> Date: Fri, 15 May 2026 13:53:44 +0200 Subject: [PATCH 01/46] Add files via upload --- src/halmos/bitvec.py | 48 +++++++++++++++++++++++++++++++++++++++++- src/halmos/contract.py | 6 +++++- src/halmos/sevm.py | 40 ++++++++++++++++++++++++++++++++++- src/halmos/utils.py | 9 +++++++- 4 files changed, 99 insertions(+), 4 deletions(-) diff --git a/src/halmos/bitvec.py b/src/halmos/bitvec.py index e1d6ac3b..4856e331 100644 --- a/src/halmos/bitvec.py +++ b/src/halmos/bitvec.py @@ -1,3 +1,4 @@ + # SPDX-License-Identifier: AGPL-3.0 from typing import Any, TypeAlias @@ -843,6 +844,51 @@ def ashr(self, shift: BV) -> "HalmosBitVec": return HalmosBitVec(self.as_z3() >> shift.value, size=self.size) + def clz(self) -> "HalmosBitVec": + """ + Count Leading Zeros — EVM opcode CLZ (0x1E, EIP-7939 / Fusaka). + + Stack semantics: [x] -> [clz(x)] + clz(0) == size (256 for a standard EVM word) + clz(1) == 255 + clz(2^255) == 0 + + Concrete: O(1) via int.bit_length(). + Symbolic: binary-search tree of 8 nested If() nodes (one per + bit-doubling level: 128/64/32/16/8/4/2/1), O(log n) Z3 terms. + + Algorithm: at each level `shift`, inspect the top `shift` bits of cur. + - If zero: add `shift` to acc, slide window left to expose next band. + - If nonzero: isolate this band for the next level. + """ + size = self._size # 256 in standard EVM context + + # --- concrete fast path --- + if self.is_concrete: + v = self._value + return HalmosBitVec(size if v == 0 else size - v.bit_length(), size=size) + + # --- symbolic path: binary search over bit-levels --- + x = self.as_z3() + zero = BitVecVal(0, size) + + acc = zero + cur = x + + for shift in (128, 64, 32, 16, 8, 4, 2, 1): + top_bits = LShR(cur, BitVecVal(size - shift, size)) + top_zero = top_bits == zero + acc = If(top_zero, acc + BitVecVal(shift, size), acc) + cur = If( + top_zero, + cur << BitVecVal(shift, size), # expose next band at MSB + top_bits << BitVecVal(size - shift, size), # keep current band aligned + ) + + # x == 0 -> every bit is a leading zero, result is `size`. + result = If(x == zero, BitVecVal(size, size), acc) + return HalmosBitVec(result, size=size) + def bitwise_not(self) -> BV: if self.is_concrete: return HalmosBitVec(~self._value & ((1 << self._size) - 1), size=self._size) @@ -961,4 +1007,4 @@ def byte(self, idx: int, *, output_size: int = 8) -> BV: FALSE.sym_val = None ZERO = HalmosBitVec(0, size=256) -ONE = HalmosBitVec(1, size=256) +ONE = HalmosBitVec(1, size=256) \ No newline at end of file diff --git a/src/halmos/contract.py b/src/halmos/contract.py index 560c22ee..0146e216 100644 --- a/src/halmos/contract.py +++ b/src/halmos/contract.py @@ -1,3 +1,4 @@ + # SPDX-License-Identifier: AGPL-3.0 from collections import defaultdict @@ -54,6 +55,7 @@ OP_SHL = 0x1B OP_SHR = 0x1C OP_SAR = 0x1D +OP_CLZ = 0x1E OP_SHA3 = 0x20 OP_ADDRESS = 0x30 OP_BALANCE = 0x31 @@ -80,6 +82,8 @@ OP_CHAINID = 0x46 OP_SELFBALANCE = 0x47 OP_BASEFEE = 0x48 +OP_BLOBHASH = 0x49 +OP_BLOBBASEFEE = 0x4A OP_POP = 0x50 OP_MLOAD = 0x51 OP_MSTORE = 0x52 @@ -536,4 +540,4 @@ def generate_lcov_report(self) -> str: # End of file lcov_lines.append("end_of_record") - return "\n".join(lcov_lines) + return "\n".join(lcov_lines) \ No newline at end of file diff --git a/src/halmos/sevm.py b/src/halmos/sevm.py index 3d2ead17..707f837f 100644 --- a/src/halmos/sevm.py +++ b/src/halmos/sevm.py @@ -1,3 +1,4 @@ + # SPDX-License-Identifier: AGPL-3.0 import itertools @@ -29,6 +30,7 @@ ArrayRef, BitVec, BitVecRef, + BitVecVal, BoolRef, BoolVal, CheckSatResult, @@ -71,6 +73,8 @@ OP_AND, OP_BALANCE, OP_BASEFEE, + OP_BLOBHASH, + OP_BLOBBASEFEE, OP_BLOCKHASH, OP_BYTE, OP_CALL, @@ -130,6 +134,7 @@ OP_RETURNDATASIZE, OP_REVERT, OP_SAR, + OP_CLZ, OP_SDIV, OP_SELFBALANCE, OP_SGT, @@ -710,6 +715,8 @@ class Block: gaslimit: BitVecRef number: BitVecRef timestamp: BitVecRef + blobbasefee: BitVecRef # EIP-4844 / Cancun — opcode 0x4A + blobhashes: list # EIP-4844 / Cancun — opcode 0x49, list of BitVecRef (256-bit) def __init__(self, **kwargs) -> None: self.basefee = kwargs["basefee"] @@ -719,6 +726,8 @@ def __init__(self, **kwargs) -> None: self.gaslimit = kwargs["gaslimit"] self.number = kwargs["number"] self.timestamp = kwargs["timestamp"] + self.blobbasefee = kwargs.get("blobbasefee", ZERO) # 0 par défaut + self.blobhashes = kwargs.get("blobhashes", []) # pas de blobs par défaut assert_address(self.coinbase) @@ -3531,6 +3540,30 @@ def finalize(ex: Exec): elif opcode == OP_BASEFEE: state.push_any(ex.block.basefee) + elif opcode == OP_BLOBBASEFEE: + # EIP-4844 / Cancun (0x4A): blob base fee du bloc courant. + state.push_any(ex.block.blobbasefee) + + elif opcode == OP_BLOBHASH: + # EIP-4844 / Cancun (0x49): versioned hash du blob a l'index i. + # L'index peut etre symbolique : on construit un If-tree sur la + # liste des blobs. Retourne 0 si hors range ou liste vide. + index = state.popi() + blobhashes = ex.block.blobhashes + if not blobhashes: + # Pas de blobs dans cette tx -> toujours 0 + state.push_any(ZERO) + else: + # Construire: If(i==0, h0, If(i==1, h1, ..., 0)) + result = ZERO + for i in range(len(blobhashes) - 1, -1, -1): + result = If( + index.as_z3() == BitVecVal(i, 256), + blobhashes[i], + result, + ) + state.push_any(result) + elif opcode == OP_PC: state.push_any(ex.pc) @@ -3578,6 +3611,11 @@ def finalize(ex: Exec): w2 = state.popi() state.push(w2.ashr(w1)) # bvashr + elif opcode == OP_CLZ: + # EIP-7939 / Fusaka (0x1E): count leading zeros. + w1 = state.popi() + state.push(w1.clz()) + elif opcode == OP_ADDMOD: w1 = state.popi() w2 = state.popi() @@ -3706,4 +3744,4 @@ def get_top_instructions(self, n: int = 20) -> list[tuple[str, int]]: return [(key, count) for key, count in self.counters.most_common(n)] def reset(self) -> None: - raise NotImplementedError("Resetting the profiler is not supported") + raise NotImplementedError("Resetting the profiler is not supported") \ No newline at end of file diff --git a/src/halmos/utils.py b/src/halmos/utils.py index b2b21278..63691f90 100644 --- a/src/halmos/utils.py +++ b/src/halmos/utils.py @@ -1,3 +1,4 @@ + # SPDX-License-Identifier: AGPL-3.0 from __future__ import annotations @@ -691,6 +692,7 @@ class EVM: SHL = 0x1B SHR = 0x1C SAR = 0x1D + CLZ = 0x1E SHA3 = 0x20 ADDRESS = 0x30 BALANCE = 0x31 @@ -717,6 +719,8 @@ class EVM: CHAINID = 0x46 SELFBALANCE = 0x47 BASEFEE = 0x48 + BLOBHASH = 0x49 + BLOBBASEFEE = 0x4A POP = 0x50 MLOAD = 0x51 MSTORE = 0x52 @@ -841,6 +845,7 @@ class EVM: EVM.SHL: "SHL", EVM.SHR: "SHR", EVM.SAR: "SAR", + EVM.CLZ: "CLZ", EVM.SHA3: "SHA3", EVM.ADDRESS: "ADDRESS", EVM.BALANCE: "BALANCE", @@ -867,6 +872,8 @@ class EVM: EVM.CHAINID: "CHAINID", EVM.SELFBALANCE: "SELFBALANCE", EVM.BASEFEE: "BASEFEE", + EVM.BLOBHASH: "BLOBHASH", + EVM.BLOBBASEFEE: "BLOBBASEFEE", EVM.POP: "POP", EVM.MCOPY: "MCOPY", EVM.MLOAD: "MLOAD", @@ -1714,4 +1721,4 @@ def wrapper(*args, **kwargs): 0xF1AFE04D: "removeFile(string)", 0x897E0A97: "writeFile(string,string)", 0x619D897F: "writeLine(string,string)", -} +} \ No newline at end of file From ce8e825c11b7eda03e92a1b3ce2978c1eb9bd3c0 Mon Sep 17 00:00:00 2001 From: HugoDowsers <123469473+HugoDowsers@users.noreply.github.com> Date: Fri, 15 May 2026 13:56:24 +0200 Subject: [PATCH 02/46] Create BUILDING.md --- BUILDING.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 BUILDING.md diff --git a/BUILDING.md b/BUILDING.md new file mode 100644 index 00000000..0021730f --- /dev/null +++ b/BUILDING.md @@ -0,0 +1 @@ +pip install -e ".[dev]" From cbee5740a33abca6259976544dea2c584916bafc Mon Sep 17 00:00:00 2001 From: HugoDowsers <123469473+HugoDowsers@users.noreply.github.com> Date: Thu, 21 May 2026 10:55:48 +0200 Subject: [PATCH 03/46] Adding Regression Tests --- .../test/OpCodesBlobBaseFee_BlobHash.t.sol | 124 ++++++++++++++++++ tests/regression/test/OpCodesCLZ.sol | 115 ++++++++++++++++ 2 files changed, 239 insertions(+) create mode 100644 tests/regression/test/OpCodesBlobBaseFee_BlobHash.t.sol create mode 100644 tests/regression/test/OpCodesCLZ.sol diff --git a/tests/regression/test/OpCodesBlobBaseFee_BlobHash.t.sol b/tests/regression/test/OpCodesBlobBaseFee_BlobHash.t.sol new file mode 100644 index 00000000..4fe2994f --- /dev/null +++ b/tests/regression/test/OpCodesBlobBaseFee_BlobHash.t.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.24 <0.9.0; + +import "forge-std/Test.sol"; +import {SymTest} from "halmos-cheatcodes/SymTest.sol"; + +// ============================================================================= +// BLOBBASEFEE (0x4A) — EIP-4844 / Cancun +// ============================================================================= + +contract BlobBaseFeeTest is SymTest, Test { + + /// In foundry env test, blobbasefee == 0 + function check_blobbasefee_zero_in_test_env() public view { + uint256 fee; + assembly { fee := blobbasefee() } + assertEq(fee, 0); + } + + /// block.blobbasefee == opcode 0x4A + function check_blobbasefee_solidity_matches_opcode() public view { + uint256 via_opcode; + assembly { via_opcode := blobbasefee() } + assertEq(via_opcode, block.blobbasefee); + } + + // ------------------------------------------------------------------------- + // Formal properties + // ------------------------------------------------------------------------- + + function check_blobbasefee_no_revert() public view { + uint256 fee; + assembly { fee := blobbasefee() } + assert(fee == fee); + } + + function check_blobbasefee_affordable(uint256 maxFee) public view { + uint256 fee; + assembly { fee := blobbasefee() } + if (fee <= maxFee) { + assert(fee <= maxFee); + } + } +} + +// ============================================================================= +// BLOBHASH (0x49) — EIP-4844 / Cancun +// ============================================================================= + +contract BlobHashTest is SymTest, Test { + + // ------------------------------------------------------------------------- + // Cas concrets (pas de blob dans l'env de test → toujours 0) + // ------------------------------------------------------------------------- + + function check_blobhash_zero_no_blob_tx() public view { + bytes32 h0; + bytes32 h5; + assembly { + h0 := blobhash(0) + h5 := blobhash(5) + } + assertEq(h0, bytes32(0)); + assertEq(h5, bytes32(0)); + } + + function check_blobhash_out_of_range() public view { + bytes32 h; + assembly { h := blobhash(6) } + assertEq(h, bytes32(0)); + } + + // ------------------------------------------------------------------------- + // Formal properties + // ------------------------------------------------------------------------- + + function check_blobhash_version_byte(uint8 index) public view { + if (index > 5) return; + bytes32 h; + uint256 idx = uint256(index); + assembly { h := blobhash(idx) } + if (h != bytes32(0)) { + // Premier octet == 0x01 (KZG commitment version) + uint8 version = uint8(uint256(h) >> 248); + assert(version == 0x01); + } + } + + /// blobhash never revert + function check_blobhash_no_revert(uint8 index) public view { + bytes32 h; + uint256 idx = uint256(index); + assembly { h := blobhash(idx) } + // reachable sans revert + assert(h == h); + } + + /// Deterministic + function check_blobhash_deterministic(uint8 index) public view { + if (index > 5) return; + uint256 idx = uint256(index); + bytes32 h1; + bytes32 h2; + assembly { + h1 := blobhash(idx) + h2 := blobhash(idx) + } + assert(h1 == h2); + } + + function check_blobhash_contiguous(uint8 index) public view { + if (index >= 5) return; + uint256 idx = uint256(index); + bytes32 hi; + bytes32 hi1; + assembly { + hi := blobhash(idx) + hi1 := blobhash(add(idx, 1)) + } + if (hi == bytes32(0)) { + assert(hi1 == bytes32(0)); + } + } +} diff --git a/tests/regression/test/OpCodesCLZ.sol b/tests/regression/test/OpCodesCLZ.sol new file mode 100644 index 00000000..24fe1fb2 --- /dev/null +++ b/tests/regression/test/OpCodesCLZ.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.24 <0.9.0; + +import "forge-std/Test.sol"; +import {SymTest} from "halmos-cheatcodes/SymTest.sol"; + +// ============================================================================= +// CLZ (0x1E) — Count Leading Zeros (EIP-7939 / Fusaka) +// ============================================================================= + + +contract ClzTest is SymTest, Test { + + function check_clz_zero() public pure { + uint256 result; + assembly { result := clz(0) } + assert(result == 256); + } + + function check_clz_one() public pure { + uint256 result; + assembly { result := clz(1) } + assert(result == 255); + } + + function check_clz_two() public pure { + uint256 result; + assembly { result := clz(2) } + assert(result == 254); + } + + function check_clz_msb() public pure { + // 2^255 + uint256 x = 1 << 255; + uint256 result; + assembly { result := clz(x) } + assert(result == 0); + } + + function check_clz_max() public pure { + // type(uint256).max + uint256 x = type(uint256).max; + uint256 result; + assembly { result := clz(x) } + assert(result == 0); + } + + function check_clz_byte() public pure { + // 0xFF = 8 bits → 256 - 8 = 248 zéros + uint256 result; + assembly { result := clz(0xFF) } + assert(result == 248); + } + + // ------------------------------------------------------------------------- + // Formal properties of the CLZ opcode + // ------------------------------------------------------------------------- + + /// clz(0) == 256 + function check_clz_zero_symbolic() public pure { + uint256 x = 0; + uint256 result; + assembly { result := clz(x) } + assert(result == 256); + } + + /// x != 0 : clz(x) ∈ [0, 255] + function check_clz_range(uint256 x) public pure { + if (x == 0) return; + uint256 result; + assembly { result := clz(x) } + assert(result <= 255); + } + + /// x != 0 : 2^(255 - clz(x)) <= x + function check_clz_lower_bound(uint256 x) public pure { + if (x == 0) return; + uint256 c; + assembly { c := clz(x) } + assert((uint256(1) << (255 - c)) <= x); + } + + /// x != 0 et clz(x) > 0 : x < 2^(256 - clz(x)) + function check_clz_upper_bound(uint256 x) public pure { + if (x == 0) return; + uint256 c; + assembly { c := clz(x) } + if (c > 0) { + assert(x < (uint256(1) << (256 - c))); + } + } + + /// clz(2^k) == 255 - k pour k ∈ [0, 255] + function check_clz_power_of_two(uint8 k) public pure { + if (k > 255) return; + uint256 p = uint256(1) << k; + uint256 result; + assembly { result := clz(p) } + assert(result == 255 - uint256(k)); + } + + /// Monotony : a <= b ⟹ clz(a) >= clz(b) + function check_clz_monotone(uint256 a, uint256 b) public pure { + if (a == 0 || b == 0) return; + if (a <= b) { + uint256 ca; uint256 cb; + assembly { + ca := clz(a) + cb := clz(b) + } + assert(ca >= cb); + } + } + +} From 47a587790a9af155c7269149e35ae03492ec702f Mon Sep 17 00:00:00 2001 From: HugoDowsers <123469473+HugoDowsers@users.noreply.github.com> Date: Thu, 21 May 2026 11:03:17 +0200 Subject: [PATCH 04/46] Add files via upload --- tests/regression/foundry.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/regression/foundry.toml b/tests/regression/foundry.toml index 18e028e3..07ac2d1a 100644 --- a/tests/regression/foundry.toml +++ b/tests/regression/foundry.toml @@ -3,7 +3,7 @@ src = 'src' out = 'out' libs = ['../lib', 'lib'] -evm_version = 'cancun' +evm_version = 'osaka' optimizer = true From 38a19236cd023d091ceafda99e92ef5732bdd469 Mon Sep 17 00:00:00 2001 From: HugoDowsers <123469473+HugoDowsers@users.noreply.github.com> Date: Thu, 17 Sep 2026 10:00:03 +0200 Subject: [PATCH 05/46] Update README.md --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e06d8844..e8af5ea5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ -# Halmos +# Dolmos + +Fork of a16z/halmos (Readme will be modified later to reflect changes) + [![PyPI - Version](https://img.shields.io/pypi/v/halmos)](https://pypi.org/project/halmos) [![Python Version from PEP 621 TOML](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2Fa16z%2Fhalmos%2Frefs%2Fheads%2Fmain%2Fpyproject.toml)](https://github.com/a16z/halmos) From e4fcc97334c8e071770565c6409e607c2abfea2b Mon Sep 17 00:00:00 2001 From: Hugo de Sa Pereira Pinto Date: Thu, 17 Sep 2026 08:08:15 +0000 Subject: [PATCH 06/46] fix: derive CREATE addresses from keccak256(rlp([sender, nonce])) halmos assigned CREATE addresses from a counter (magic_address + n) and did not model account nonces at all. A contract that predicts its own deployment address therefore disagreed with the executor, which breaks every CREATE3 library: the proxy deploys somewhere the caller cannot find, the `codesize != 0` check fails and all paths revert (paths: 0). Exec now tracks nonces (1 for a newly created account per EIP-161, incremented by both CREATE and CREATE2 even when the creation fails), and new_address() routes the rlp encoding of [sender, nonce] through sha3_data(), which maps it to a magic address exactly like the 0xff-prefixed 85-byte preimage of CREATE2. Both sides of the prediction then land on the same address, whatever the order in which they are computed. Closes #217 --- src/halmos/__main__.py | 3 ++ src/halmos/sevm.py | 117 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 116 insertions(+), 4 deletions(-) diff --git a/src/halmos/__main__.py b/src/halmos/__main__.py index d768243d..9acc6f23 100644 --- a/src/halmos/__main__.py +++ b/src/halmos/__main__.py @@ -293,6 +293,9 @@ def deploy_test(ctx: FunctionContext, sevm: SEVM) -> Exec: # foundry default balance for the test contract ex.balance_update(this, con(0xFFFFFFFFFFFFFFFFFFFFFFFF)) + # the test contract is a deployed contract, so its nonce starts at 1 (EIP-161) + ex.set_nonce(this, 1) + # deploy libraries and resolve library placeholders in hexcode contract_ctx = ctx.contract_ctx (creation_hexcode, _) = ex.resolve_libs( diff --git a/src/halmos/sevm.py b/src/halmos/sevm.py index 707f837f..47daf4b2 100644 --- a/src/halmos/sevm.py +++ b/src/halmos/sevm.py @@ -301,9 +301,68 @@ create2_magic_address: int = 0xBBBB0000 +create_magic_address: int = 0xCCCC0000 + new_address_offset: int = 1 +def rlp_create_preimage(sender: Address, nonce: int) -> BitVecRef: + """ + Returns the RLP encoding of [sender, nonce], i.e. the preimage of the keccak hash + that determines the address of a contract deployed with CREATE. + + This is the encoding that contracts use to predict CREATE addresses themselves, + e.g. CREATE3 libraries computing keccak256(abi.encodePacked(hex"d694", proxy, hex"01")). + """ + + if nonce < 0: + raise ValueError(nonce) + + if nonce == 0: + # the empty string is encoded as 0x80 + nonce_rlp = con(0x80, 8) + elif nonce < 0x80: + # a single byte below 0x80 is its own encoding + nonce_rlp = con(nonce, 8) + else: + num_bytes = (nonce.bit_length() + 7) // 8 + nonce_rlp = Concat(con(0x80 + num_bytes, 8), con(nonce, 8 * num_bytes)) + + # 1 byte for the address prefix (0x94) + 20 address bytes + the encoded nonce + payload_size = 21 + nonce_rlp.size() // 8 + + return simplify( + Concat( + con(0xC0 + payload_size, 8), + con(0x94, 8), + uint160(sender).as_z3(), + nonce_rlp, + ) + ) + + +def is_rlp_create_preimage(data: Bytes, size: int) -> bool: + """ + Returns true if the given data looks like the RLP encoding of [address, nonce], + i.e. `0xc0+n 0x94 <20-byte address> `. + """ + + # 23 bytes for a nonce in [0, 0x7f], more for larger nonces + if not (23 <= size <= 32): + return False + + data = ByteVec(data) + list_prefix = unbox_int(data.get_byte(0)) + addr_prefix = unbox_int(data.get_byte(1)) + + return ( + isinstance(list_prefix, int) + and isinstance(addr_prefix, int) + and list_prefix == 0xC0 + size - 1 + and addr_prefix == 0x94 + ) + + def jumpid_str(jumpid: JumpID) -> str: pc, jumpdests = jumpid return f"{pc}:{','.join(str(j) for j in jumpdests)}" @@ -1191,6 +1250,7 @@ class Exec: # an execution path # internal bookkeeping cnts: dict[str, int] # counters + nonces: dict[str, int] # account nonces, used for CREATE address derivation sha3s: KeccakRegistry # sha3 hashes generated storages: dict[Any, Any] # storage updates balances: dict[Any, Any] # balance updates @@ -1227,6 +1287,10 @@ def __init__(self, **kwargs) -> None: self.alias = kwargs["alias"] # self.cnts = kwargs["cnts"] + # note: an empty dict must be preserved as-is, so that sub-contexts keep + # sharing the same nonce map as their parent frame + nonces = kwargs.get("nonces") + self.nonces = nonces if nonces is not None else defaultdict(int) self.sha3s = kwargs["sha3s"] self.storages = kwargs["storages"] self.balances = kwargs["balances"] @@ -1556,6 +1620,12 @@ def sha3_data(self, data: Bytes) -> Word: if isinstance(first_byte, int) and first_byte == 0xFF: return con(create2_magic_address + self.sha3s.get_id(sha3_expr)) + # handle create hash: keccak256(rlp([sender, nonce])) + # Exec.new_address() returns the same magic address, so that contracts + # predicting CREATE addresses (e.g. CREATE3 libraries) agree with the executor + if is_rlp_create_preimage(data, size): + return con(create_magic_address + self.sha3s.get_id(sha3_expr)) + # return the concrete hash value if available, otherwise return the hash expression return sha3_hash_bv if sha3_hash is not None else sha3_expr @@ -1596,9 +1666,36 @@ def new_gas_id(self) -> int: self.cnts["gas"] += 1 return self.cnts["gas"] - def new_address(self) -> Address: - self.cnts["address"] += 1 - return con_addr(magic_address + new_address_offset + self.cnts["address"]) + def nonce_key(self, addr: Address) -> str: + # normalize the address, so that BV and z3 representations map to the same key + return str(uint160(addr).as_z3()) + + def nonce_of(self, addr: Address) -> int: + return self.nonces[self.nonce_key(addr)] + + def set_nonce(self, addr: Address, nonce: int) -> None: + self.nonces[self.nonce_key(addr)] = nonce + + def bump_nonce(self, addr: Address) -> None: + self.nonces[self.nonce_key(addr)] += 1 + + def new_address(self, sender: Address | None = None) -> Address: + """ + Returns the address of the contract to be created by `sender` with CREATE. + + The address is derived from keccak256(rlp([sender, nonce])) as in the EVM, + except that sha3_data() maps the hash to a magic address (see sha3_data). + This keeps addresses concrete and short while staying consistent with + contracts that predict CREATE addresses themselves. + """ + + if sender is None: + # legacy behavior, used when no sender is available (e.g. library deployment) + self.cnts["address"] += 1 + return con_addr(magic_address + new_address_offset + self.cnts["address"]) + + preimage = rlp_create_preimage(sender, self.nonce_of(sender)) + return uint160(self.sha3_data(preimage)).as_z3() def new_symbol_id(self) -> int: self.cnts["symbol"] += 1 @@ -2460,6 +2557,7 @@ def callback(new_ex: Exec, stack): alias=ex.alias, # cnts=ex.cnts, + nonces=ex.nonces, sha3s=ex.sha3s, storages=ex.storages, balances=ex.balances, @@ -2689,7 +2787,7 @@ def create( # new account address if op == OP_CREATE: - new_addr = ex.new_address() + new_addr = ex.new_address(pranked_caller) elif op == OP_CREATE2: # OP_CREATE2 # create_hexcode must be z3 expression to be passed into sha3_data create_hexcode = create_hexcode.unwrap() @@ -2712,6 +2810,10 @@ def create( else: raise HalmosException(f"Unknown CREATE opcode: {op}") + # both CREATE and CREATE2 increment the creator's nonce, + # regardless of whether the creation succeeds + ex.bump_nonce(pranked_caller) + message = Message( target=new_addr, caller=pranked_caller, @@ -2750,6 +2852,9 @@ def create( # setup new account ex.set_code(new_addr, Contract(b"")) # existing code must be empty + # a newly created account starts with nonce 1 (EIP-161) + ex.set_nonce(new_addr, 1) + # existing storage may not be empty and reset here ex.storage[new_addr] = self.mk_storagedata() ex.transient_storage[new_addr] = self.mk_storagedata() @@ -2826,6 +2931,7 @@ def callback(new_ex: Exec, stack): alias=ex.alias, # cnts=ex.cnts, + nonces=ex.nonces, sha3s=ex.sha3s, storages=ex.storages, balances=ex.balances, @@ -2949,6 +3055,7 @@ def create_branch(self, ex: Exec, cond: BitVecRef, target: int) -> Exec: alias=ex.alias.copy(), # cnts=deepcopy(ex.cnts), + nonces=deepcopy(ex.nonces), sha3s=ex.sha3s.copy(), storages=ex.storages.copy(), balances=ex.balances.copy(), @@ -3040,6 +3147,7 @@ def run_message(self, pre_ex: Exec, message: Message, path: Path) -> Iterator[Ex alias=pre_ex.alias.copy(), # cnts=deepcopy(pre_ex.cnts), + nonces=deepcopy(pre_ex.nonces), sha3s=pre_ex.sha3s.copy(), storages=pre_ex.storages.copy(), balances=pre_ex.balances.copy(), @@ -3714,6 +3822,7 @@ def mk_exec( # log=[], cnts=defaultdict(int), + nonces=defaultdict(int), sha3s=KeccakRegistry(), storages={}, balances={}, From b7ef854a444c256da0d37fbc53b834e47e6d58c0 Mon Sep 17 00:00:00 2001 From: Hugo de Sa Pereira Pinto Date: Thu, 17 Sep 2026 08:08:15 +0000 Subject: [PATCH 07/46] feat: support vm.getNonce / setNonce / setNonceUnsafe / resetNonce Now that nonces are modeled, expose foundry's nonce cheatcodes. Selectors taken from foundry's crates/cheatcodes/assets/cheatcodes.json. setNonce() refuses to lower a nonce, as in foundry, and points at setNonceUnsafe(); resetNonce() sets 0 for EOAs and 1 for accounts with code. --- src/halmos/cheatcodes.py | 70 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/halmos/cheatcodes.py b/src/halmos/cheatcodes.py index 73452180..39bfae32 100644 --- a/src/halmos/cheatcodes.py +++ b/src/halmos/cheatcodes.py @@ -958,6 +958,21 @@ class hevm_cheat_code: # bytes4(keccak256("deal(address,uint256)")) deal_sig: int = 0xC88A5E6D + # bytes4(keccak256("getNonce(address)")) + get_nonce_sig: int = 0x2D0335AB + + # bytes4(keccak256("getNonce((address,uint256,uint256,uint256))")) + get_nonce_wallet_sig: int = 0xA5748AAD + + # bytes4(keccak256("setNonce(address,uint64)")) + set_nonce_sig: int = 0xF8E18B57 + + # bytes4(keccak256("setNonceUnsafe(address,uint64)")) + set_nonce_unsafe_sig: int = 0x9B67B21C + + # bytes4(keccak256("resetNonce(address)")) + reset_nonce_sig: int = 0x1C72346D + # bytes4(keccak256("store(address,bytes32,bytes32)")) store_sig: int = 0x70CA10BB @@ -1205,6 +1220,61 @@ def handle(sevm, ex, arg: ByteVec, stack) -> ByteVec | None: ex.balance_update(who, amount) return ret + # vm.getNonce(address) / vm.getNonce(Wallet) + elif funsig in ( + hevm_cheat_code.get_nonce_sig, + hevm_cheat_code.get_nonce_wallet_sig, + ): + # Wallet is a static struct, its first field is the address + who = uint160(arg.get_word(4)).as_z3() + return ByteVec(con(ex.nonce_of(who))) + + # vm.setNonce(address,uint64) / vm.setNonceUnsafe(address,uint64) + elif funsig in ( + hevm_cheat_code.set_nonce_sig, + hevm_cheat_code.set_nonce_unsafe_sig, + ): + unsafe = funsig == hevm_cheat_code.set_nonce_unsafe_sig + name = "setNonceUnsafe" if unsafe else "setNonce" + + who = uint160(arg.get_word(4)) + if not who.is_concrete: + error_msg = f"vm.{name}(address who, uint64 nonce) must have concrete argument `who` but received {who}" + raise HalmosException(error_msg) + + nonce = int_of(arg.get_word(36), f"symbolic nonce in vm.{name}()") + if not (0 <= nonce < 2**64): + raise HalmosException(f"vm.{name}(): nonce out of uint64 range: {nonce}") + + who = who.as_z3() + current = ex.nonce_of(who) + + # vm.setNonce() can only increase the nonce, as in foundry + if not unsafe and nonce < current: + error_msg = ( + f"vm.setNonce(): the new nonce ({nonce}) must be greater than or equal to " + f"the current nonce ({current}) of {hexify(who)}; " + f"use vm.setNonceUnsafe() to decrease it" + ) + raise HalmosException(error_msg) + + ex.set_nonce(who, nonce) + return ret + + # vm.resetNonce(address) + elif funsig == hevm_cheat_code.reset_nonce_sig: + who = uint160(arg.get_word(4)) + if not who.is_concrete: + error_msg = f"vm.resetNonce(address who) must have concrete argument `who` but received {who}" + raise HalmosException(error_msg) + + who = who.as_z3() + + # 0 for EOAs, 1 for contract accounts + code = ex.code.get(who) + ex.set_nonce(who, 1 if code is not None and len(code) > 0 else 0) + return ret + # vm.store(address,bytes32,bytes32) elif funsig == hevm_cheat_code.store_sig: if arg == hevm_cheat_code.fail_payload: From 44334d10342d922bea2e64378ba14fd95dad1419 Mon Sep 17 00:00:00 2001 From: Hugo de Sa Pereira Pinto Date: Thu, 17 Sep 2026 08:08:15 +0000 Subject: [PATCH 08/46] test: regression tests for CREATE3 and the nonce cheatcodes Covers the CREATE3 deployment pattern reported in #217, direct prediction of `new C()` at nonces 1 and 2, and the four nonce cheatcodes. --- tests/regression/test/Create3.t.sol | 144 ++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 tests/regression/test/Create3.t.sol diff --git a/tests/regression/test/Create3.t.sol b/tests/regression/test/Create3.t.sol new file mode 100644 index 00000000..20983ad7 --- /dev/null +++ b/tests/regression/test/Create3.t.sol @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import "forge-std/Test.sol"; + +/// @notice minimal CREATE3 library, as found in 0xsequence/create3, solmate and solady +/// @dev see https://github.com/a16z/halmos/issues/217 +library Create3 { + bytes internal constant PROXY_CHILD_BYTECODE = hex"67363d3d37363d34f03d5260086018f3"; + + bytes32 internal constant KECCAK256_PROXY_CHILD_BYTECODE = + 0x21c35dbe1b344a2488cf3321d6ce542f8e9f305544ff09e4993a62319a497c1f; + + /// @dev keccak256(rlp([keccak256(0xff ++ address(this) ++ salt ++ keccak256(childBytecode))[12:], 0x01])) + function addressOf(bytes32 salt) internal view returns (address) { + address proxy = address( + uint160(uint256(keccak256(abi.encodePacked(hex"ff", address(this), salt, KECCAK256_PROXY_CHILD_BYTECODE)))) + ); + + return address(uint160(uint256(keccak256(abi.encodePacked(hex"d694", proxy, hex"01"))))); + } + + function create3(bytes32 salt, bytes memory creationCode) internal returns (address addr) { + bytes memory proxyCode = PROXY_CHILD_BYTECODE; + + addr = addressOf(salt); + require(addr.code.length == 0, "TargetAlreadyExists"); + + address proxy; + assembly { + proxy := create2(0, add(proxyCode, 32), mload(proxyCode), salt) + } + require(proxy != address(0), "ErrorCreatingProxy"); + + (bool success,) = proxy.call(creationCode); + require(success && addr.code.length != 0, "ErrorCreatingContract"); + } +} + +contract Counter { + uint256 public number; + + constructor(uint256 _number) { + number = _number; + } + + function increment() public { + number++; + } +} + +contract Create3Test is Test { + function check_create3(uint256 initial) public { + vm.assume(initial < type(uint256).max); + + bytes32 salt = keccak256("counter"); + + address predicted = Create3.addressOf(salt); + address deployed = Create3.create3(salt, abi.encodePacked(type(Counter).creationCode, abi.encode(initial))); + + assertEq(deployed, predicted); + assertEq(Counter(deployed).number(), initial); + + Counter(deployed).increment(); + assertEq(Counter(deployed).number(), initial + 1); + } + + /// @notice a CREATE deployment must land on keccak256(rlp([sender, nonce])) + function check_create_address_prediction() public { + // the test contract has nonce 1 before its first deployment + address predicted = address(uint160(uint256(keccak256(abi.encodePacked(hex"d694", address(this), hex"01"))))); + + Counter c = new Counter(0); + + assertEq(address(c), predicted); + } + + function check_create_address_prediction_nonce2() public { + new Counter(0); + + address predicted = address(uint160(uint256(keccak256(abi.encodePacked(hex"d694", address(this), hex"02"))))); + + Counter c = new Counter(0); + + assertEq(address(c), predicted); + } +} + +contract NonceTest is Test { + function check_nonce_of_test_contract() public { + assertEq(vm.getNonce(address(this)), 1); + + new Counter(0); + assertEq(vm.getNonce(address(this)), 2); + + new Counter(0); + assertEq(vm.getNonce(address(this)), 3); + } + + function check_nonce_of_created_contract() public { + Counter c = new Counter(0); + + // a newly created account starts at nonce 1 (EIP-161) + assertEq(vm.getNonce(address(c)), 1); + } + + function check_setNonce() public { + address eoa = address(0x1234); + assertEq(vm.getNonce(eoa), 0); + + vm.setNonce(eoa, 7); + assertEq(vm.getNonce(eoa), 7); + + // setNonce() can only increase the nonce, setNonceUnsafe() can decrease it + vm.setNonceUnsafe(eoa, 3); + assertEq(vm.getNonce(eoa), 3); + + vm.resetNonce(eoa); + assertEq(vm.getNonce(eoa), 0); + } + + function check_resetNonce_of_contract() public { + Counter c = new Counter(0); + + vm.setNonce(address(c), 9); + assertEq(vm.getNonce(address(c)), 9); + + // resets to 1 for contract accounts + vm.resetNonce(address(c)); + assertEq(vm.getNonce(address(c)), 1); + } + + /// @notice setNonce() must change the address of the next deployment + function check_setNonce_changes_create_address() public { + vm.setNonce(address(this), 42); + + address predicted = + address(uint160(uint256(keccak256(abi.encodePacked(hex"d694", address(this), bytes1(0x2a)))))); + + Counter c = new Counter(0); + + assertEq(address(c), predicted); + } +} From 4bb7b35ba910fcdb8b0fb6022daa2d9d7cb66c93 Mon Sep 17 00:00:00 2001 From: Hugo de Sa Pereira Pinto Date: Thu, 17 Sep 2026 08:08:21 +0000 Subject: [PATCH 09/46] chore(dowsers): expose the CLI as `dolmos` Second console script pointing at the same entry point, so `halmos` keeps working. `--help` now shows the name the command was invoked with, and dolmos.toml is read before halmos.toml in the project root. Dowsers-only: never propose upstream. --- pyproject.toml | 1 + src/halmos/config.py | 21 +++++++++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 69cb98a4..ce50199f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ dynamic = ["version"] [project.scripts] halmos = "halmos.__main__:main" +dolmos = "halmos.__main__:main" [project.urls] "Homepage" = "https://github.com/a16z/halmos" diff --git a/src/halmos/config.py b/src/halmos/config.py index ff9cde8f..ce0cd5cb 100644 --- a/src/halmos/config.py +++ b/src/halmos/config.py @@ -69,6 +69,7 @@ def arg( countable: bool = False, global_default_str: str | None = None, action: Callable = None, + extra_names: list[str] | None = None, ): return dataclass_field( default=None, @@ -79,6 +80,7 @@ def arg( "group": group, "choices": choices, "short": short, + "extra_names": extra_names or [], "countable": countable, "global_default_str": global_default_str, "action": action, @@ -287,6 +289,7 @@ class Config: global_default="", metavar="FUNCTION_NAME_REGEX", short="mt", + extra_names=["-test"], ) panic_error_codes: str = arg( @@ -775,12 +778,15 @@ def resolve_config_files(args: list[str], include_missing: bool = False) -> list if args.config: return [args.config] - # we expect to find halmos.toml in the project root directory - default_config_path = os.path.join(args.root, "halmos.toml") - if not include_missing and not os.path.exists(default_config_path): - return [] + # we expect to find dolmos.toml (or halmos.toml) in the project root directory + candidates = [ + os.path.join(args.root, name) for name in ("dolmos.toml", "halmos.toml") + ] + for path in candidates: + if os.path.exists(path): + return [path] - return [default_config_path] + return candidates[:1] if include_missing else [] class TomlParser: @@ -849,7 +855,8 @@ def _create_default_config() -> "Config": def _create_arg_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( - prog="halmos", + # use the name the command was invoked with (halmos, dolmos, ...) + prog=os.path.basename(sys.argv[0]) or "halmos", epilog="For more information, see https://github.com/a16z/halmos", ) @@ -870,6 +877,8 @@ def _create_arg_parser() -> argparse.ArgumentParser: if short_name: names.append(f"-{short_name}") + names.extend(field_info.metadata.get("extra_names", [])) + arg_help = field_info.metadata.get("help", "") metavar = field_info.metadata.get("metavar", None) group_name = field_info.metadata.get("group", None) From 21c3f791fb22cd5a001e3a89715b833d3379e468 Mon Sep 17 00:00:00 2001 From: Hugo de Sa Pereira Pinto Date: Thu, 17 Sep 2026 08:53:58 +0000 Subject: [PATCH 10/46] chore(dowsers): drop the legacy `halmos` console script Only `dolmos` is installed now. The python package keeps its upstream name `halmos`, so imports and rebases on upstream are unaffected; only the command disappears. Note: the docker smoke tests in .github/workflows still run `halmos --version` and will fail on this fork until they are updated. --- pyproject.toml | 1 - src/halmos/__main__.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ce50199f..2c0b214a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,6 @@ dependencies = [ dynamic = ["version"] [project.scripts] -halmos = "halmos.__main__:main" dolmos = "halmos.__main__:main" [project.urls] diff --git a/src/halmos/__main__.py b/src/halmos/__main__.py index 9acc6f23..b1e5ea19 100644 --- a/src/halmos/__main__.py +++ b/src/halmos/__main__.py @@ -1732,7 +1732,8 @@ def _main(_args=None) -> MainResult: args = load_config(_args) if args.version: - print(f"halmos {metadata.version('halmos')}") + # report the name the command was invoked with, the distribution is still halmos + print(f"{os.path.basename(sys.argv[0]) or 'dolmos'} {metadata.version('halmos')}") return MainResult(0) init_env(args.root) From 8d2282eef8004fa244876480d9abd5401e5df763 Mon Sep 17 00:00:00 2001 From: Hugo DE SA PEREIRA PINTO Date: Thu, 17 Sep 2026 11:12:52 +0200 Subject: [PATCH 11/46] Readme update --- README.md | 197 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 128 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index e8af5ea5..de51dee5 100644 --- a/README.md +++ b/README.md @@ -1,105 +1,164 @@ # Dolmos -Fork of a16z/halmos (Readme will be modified later to reflect changes) - - -[![PyPI - Version](https://img.shields.io/pypi/v/halmos)](https://pypi.org/project/halmos) -[![Python Version from PEP 621 TOML](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2Fa16z%2Fhalmos%2Frefs%2Fheads%2Fmain%2Fpyproject.toml)](https://github.com/a16z/halmos) -[![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) - -[**Install**](https://github.com/a16z/halmos?tab=readme-ov-file#installation) -| [**Getting Started**](https://github.com/a16z/halmos/blob/main/docs/getting-started.md) -| [**Examples**](https://github.com/a16z/halmos/blob/main/examples/README.md) -| [**FAQ**](https://github.com/a16z/halmos/wiki/FAQ) -| [**Chat**][chat] -| [**awesome-halmos**](https://github.com/redtrama/awesome-halmos) - -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. - -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. - -[post]: https://a16zcrypto.com/symbolic-testing-with-halmos-leveraging-existing-tests-for-formal-verification/ -[chat]: https://t.me/+4UhzHduai3MzZmUx +[![License](https://img.shields.io/github/license/HugoDowsers/dolmos)](LICENSE) +[![Python Version from PEP 621 TOML](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2FHugoDowsers%2Fdolmos%2Frefs%2Fheads%2Fmain%2Fpyproject.toml)](pyproject.toml) +[![upstream](https://img.shields.io/badge/fork%20of-a16z%2Fhalmos-blue)](https://github.com/a16z/halmos) + +Dolmos is the [Dowsers](https://github.com/HugoDowsers) fork of +[a16z/halmos](https://github.com/a16z/halmos), a _symbolic testing_ tool for EVM +smart contracts driven by Foundry tests. + +The fork exists to carry fixes and EVM coverage we need for audit work, ahead of +(or instead of) upstream. Everything upstream does, dolmos does; this README only +documents what differs. For the tool itself, read the upstream +[getting started guide](docs/getting-started.md) and the +[examples](examples/README.md) — they apply unchanged, modulo the command name. + +The command is `dolmos`. The Python package is still named `halmos`, so +`import halmos` and rebases on upstream keep working. + +## What this fork adds + +**CREATE address derivation and account nonces.** Upstream assigns CREATE +addresses from a counter and does not model nonces at all, so a contract that +predicts its own deployment address disagrees with the executor. Every CREATE3 +library breaks that way ([a16z/halmos#217](https://github.com/a16z/halmos/issues/217)): +all paths revert and the test reports `paths: 0`. Dolmos tracks nonces (1 for a +newly created account per EIP-161, incremented by CREATE and CREATE2 even when +the creation fails) and derives the address from `keccak256(rlp([sender, nonce]))`, +mapped to a magic address exactly as CREATE2 already was. Both sides of the +prediction then agree. + +**Nonce cheatcodes.** `vm.getNonce`, `vm.setNonce`, `vm.setNonceUnsafe` and +`vm.resetNonce`, with foundry's semantics: `setNonce` only raises a nonce, +`resetNonce` gives 0 to EOAs and 1 to accounts with code. + +**Fusaka / Cancun opcodes.** `CLZ` (EIP-7939), with a concrete fast path and a +logarithmic symbolic encoding, plus `BLOBHASH` and `BLOBBASEFEE` (EIP-4844). + +**CLI.** The command is `dolmos`; `halmos` is no longer installed. A project +config file may be named `dolmos.toml` (read before `halmos.toml`), `--help` and +`--version` report the name the command was invoked with, and `-test` is a short +alias for `--match-test` (`dolmos -test setNonce`). + +## Not available in this fork + +These exist upstream and are **not** provided here yet: + +| | status | +|---|---| +| PyPI package (`pip install dolmos`) | not published — install from source | +| `uv tool install dolmos` | not available — depends on the PyPI package | +| Docker image (`ghcr.io/...`) | not built — no published image | +| Prebuilt release binaries | none | +| CI workflows | inherited from upstream and currently broken on this fork, since they invoke the `halmos` command and the a16z docker image | + +Until then, install from source as described below. If you want the packaged +experience today, use upstream halmos instead — the two can coexist in the same +environment, they no longer share a command name. ## Installation -### ⭐ Using `uv` (recommended for most users) +Requires Python ≥ 3.11 and [Foundry](https://getfoundry.sh). ```sh -# install uv if you don't have it already -curl -LsSf https://astral.sh/uv/install.sh | sh - -# install the latest version of halmos for the current user and add it to PATH -uv tool install --python 3.12 halmos - -# or, install the development version from the repository -# uv tool install --python 3.12 git+https://github.com/a16z/halmos - -# after installing, you can update halmos to the latest version with: -uv tool upgrade halmos +git clone https://github.com/HugoDowsers/dolmos.git +cd dolmos +python3.12 -m venv .venv && source .venv/bin/activate +pip install -e . +dolmos --version ``` -### Using `docker` - -You can download a pre-built Docker image that contains python, halmos, its dependencies, foundry, solvers, etc.: +Development install, with the test and lint dependencies: ```sh -docker pull ghcr.io/a16z/halmos:latest +pip install -e ".[dev]" +pre-commit install ``` -### Using `pip` (for advanced users) +If a `halmos` command lingers in your `PATH` after installing, it comes from a +separate installation (typically a `pip install --user` one). Check with +`type -a halmos`, and `python -c "import halmos, os; print(os.path.dirname(halmos.__file__))"` +to confirm which source tree is actually being imported. -Note: this is not recommended because of the extra work required to manage the python version and the virtual environment. But if you know what you are doing, and need the extra control, you can do it like this: +## Usage ```sh -# make sure you have a suitable python version installed, e.g.: -python3.12 --version - -# create and activate a virtual environment with an explicit python version -python3.12 -m venv .venv && source .venv/bin/activate - -# install the latest version of halmos -pip install halmos +cd /path/to/foundry/project +forge build +dolmos +``` -# or, install the development version from the repository -pip install git+https://github.com/a16z/halmos +```sh +dolmos --help +dolmos -test # alias for --match-test +dolmos --match-contract ``` -## Usage +Options can be set per project in `dolmos.toml` at the project root, with the +same format as upstream's `halmos.toml`: -```sh -cd /path/to/src -halmos +```toml +[global] +solver-timeout-assertion = 10000 ``` -For more details: +## Tests + +The regression suite lives in `tests/regression` and runs with both forge (real +EVM) and dolmos (symbolic): ```sh -halmos --help +cd tests/regression +forge build +dolmos ``` -Alternatively, you can run the latest halmos Docker image available at [ghcr.io/a16z/halmos](https://ghcr.io/a16z/halmos): +Two notes on the toolchain. The suite sets `evm_version = 'osaka'` because of +the CLZ tests, and `test/OpCodesCLZ.sol` needs a solc recent enough to accept +`clz()` in assembly — an older compiler fails with `Function "clz" not found`. +And `test/Invalid.t.sol`, inherited from upstream, requires solc `^0.5.2`; if +that version is unavailable, `forge build` stops there. -```sh -cd /path/to/src +## Contributing -# mount '.' under /workspace in the container -docker run -v .:/workspace ghcr.io/a16z/halmos:latest -``` +Fixes that are not specific to Dowsers should go upstream: branch off +`upstream/main`, keep the branch free of any fork-specific change, and open the +pull request against +[a16z/halmos](https://github.com/a16z/halmos). The CREATE3 work above is written +that way and is meant to be proposed. + +See the upstream [contributing guidelines](CONTRIBUTING.md) for style, tests and +commit conventions. -## Getting Started +## Roadmap -Refer to the [getting started guide](docs/getting-started.md) and the [examples](examples/README.md) directory. +Work in progress, not merged here yet: -## Contributing / Developing +- MODEXP precompile (EIP-198): concrete evaluation and a typed uninterpreted + function instead of the current opaque one +- performance: skip the Z3 `substitute()` traversal when no free variable + matches, bound the concurrency of solver subprocesses, memoize invariant + target resolution +- support multiple `setUp()` states ([a16z/halmos#186](https://github.com/a16z/halmos/issues/186)) +- docker image and a published package -Refer to the [contributing guidelines](CONTRIBUTING.md), and explore the list of issues labeled ["good first issue" or "help wanted."][issues] +## License -[issues]: https://github.com/a16z/halmos/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22%2C%22help+wanted%22 +AGPL-3.0, inherited from upstream. See [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._ +_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._ + +This fork is maintained by Dowsers and is not affiliated with or endorsed by +a16z. The upstream disclaimer above applies to the original work; the same lack +of warranty applies to the changes made here. From 3dfa9d5337e4490af91ec9fb39d3517c66d88052 Mon Sep 17 00:00:00 2001 From: HugoDowsers <123469473+HugoDowsers@users.noreply.github.com> Date: Thu, 17 Sep 2026 11:24:06 +0200 Subject: [PATCH 12/46] Update README.md --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index de51dee5..6b3578c1 100644 --- a/README.md +++ b/README.md @@ -128,9 +128,6 @@ pull request against [a16z/halmos](https://github.com/a16z/halmos). The CREATE3 work above is written that way and is meant to be proposed. -See the upstream [contributing guidelines](CONTRIBUTING.md) for style, tests and -commit conventions. - ## Roadmap Work in progress, not merged here yet: From 920ad52a6115354314be12104778e04ffcc33572 Mon Sep 17 00:00:00 2001 From: HugoDowsers <123469473+HugoDowsers@users.noreply.github.com> Date: Thu, 17 Sep 2026 11:24:30 +0200 Subject: [PATCH 13/46] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6b3578c1..b307c48e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Python Version from PEP 621 TOML](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2FHugoDowsers%2Fdolmos%2Frefs%2Fheads%2Fmain%2Fpyproject.toml)](pyproject.toml) [![upstream](https://img.shields.io/badge/fork%20of-a16z%2Fhalmos-blue)](https://github.com/a16z/halmos) -Dolmos is the [Dowsers](https://github.com/HugoDowsers) fork of +Dolmos is the [Dowsers](https://github.com/Dowsers) fork of [a16z/halmos](https://github.com/a16z/halmos), a _symbolic testing_ tool for EVM smart contracts driven by Foundry tests. From d5c5407302744312dfad8ff3e5a65c8dfd2a1956 Mon Sep 17 00:00:00 2001 From: Hugo de Sa Pereira Pinto Date: Thu, 17 Sep 2026 09:47:26 +0000 Subject: [PATCH 14/46] chore(dowsers): rename the distribution to dolmos pip list, pip uninstall and importlib.metadata now see `dolmos`, and --version reads the version from that distribution. Homepage points at the fork, with the upstream repository kept as a separate URL. The importable package stays src/halmos: `import halmos` is unchanged and rebases on upstream do not conflict over a renamed directory. --- pyproject.toml | 7 ++++--- src/halmos/__main__.py | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2c0b214a..eb7997b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,8 +5,8 @@ build-backend = "setuptools.build_meta" [tool.setuptools_scm] [project] -name = "halmos" -description = "A symbolic testing tool for EVM smart contracts" +name = "dolmos" +description = "A symbolic testing tool for EVM smart contracts (Dowsers fork of halmos)" readme = "README.md" authors = [ { name="a16z crypto" }, @@ -39,7 +39,8 @@ dynamic = ["version"] dolmos = "halmos.__main__:main" [project.urls] -"Homepage" = "https://github.com/a16z/halmos" +"Homepage" = "https://github.com/HugoDowsers/dolmos" +"Upstream" = "https://github.com/a16z/halmos" # development dependencies, can be installed with # uv sync --extra dev diff --git a/src/halmos/__main__.py b/src/halmos/__main__.py index b1e5ea19..2e5de109 100644 --- a/src/halmos/__main__.py +++ b/src/halmos/__main__.py @@ -1732,8 +1732,8 @@ def _main(_args=None) -> MainResult: args = load_config(_args) if args.version: - # report the name the command was invoked with, the distribution is still halmos - print(f"{os.path.basename(sys.argv[0]) or 'dolmos'} {metadata.version('halmos')}") + # the distribution is dolmos, the importable package is still halmos + print(f"{os.path.basename(sys.argv[0]) or 'dolmos'} {metadata.version('dolmos')}") return MainResult(0) init_env(args.root) From 3690cd8bf56f4be5c9bf237b56ec2bde1413396d Mon Sep 17 00:00:00 2001 From: Hugo de Sa Pereira Pinto Date: Thu, 17 Sep 2026 09:47:26 +0000 Subject: [PATCH 15/46] docs: point the Dowsers link at the right account --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b307c48e..6b3578c1 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Python Version from PEP 621 TOML](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2FHugoDowsers%2Fdolmos%2Frefs%2Fheads%2Fmain%2Fpyproject.toml)](pyproject.toml) [![upstream](https://img.shields.io/badge/fork%20of-a16z%2Fhalmos-blue)](https://github.com/a16z/halmos) -Dolmos is the [Dowsers](https://github.com/Dowsers) fork of +Dolmos is the [Dowsers](https://github.com/HugoDowsers) fork of [a16z/halmos](https://github.com/a16z/halmos), a _symbolic testing_ tool for EVM smart contracts driven by Foundry tests. From 34287b52eba8002c1d63c9578d581277e7018fad Mon Sep 17 00:00:00 2001 From: HugoDowsers <123469473+HugoDowsers@users.noreply.github.com> Date: Fri, 18 Sep 2026 10:01:08 +0200 Subject: [PATCH 16/46] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6b3578c1..b307c48e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Python Version from PEP 621 TOML](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2FHugoDowsers%2Fdolmos%2Frefs%2Fheads%2Fmain%2Fpyproject.toml)](pyproject.toml) [![upstream](https://img.shields.io/badge/fork%20of-a16z%2Fhalmos-blue)](https://github.com/a16z/halmos) -Dolmos is the [Dowsers](https://github.com/HugoDowsers) fork of +Dolmos is the [Dowsers](https://github.com/Dowsers) fork of [a16z/halmos](https://github.com/a16z/halmos), a _symbolic testing_ tool for EVM smart contracts driven by Foundry tests. From 139c6da4f6fa99f5529282fffd1dacff20dfaf24 Mon Sep 17 00:00:00 2001 From: Hugo DE SA PEREIRA PINTO Date: Mon, 21 Sep 2026 21:29:44 +0200 Subject: [PATCH 17/46] Adding Vyper - Experimental --- README.md | 28 +++ src/halmos/__main__.py | 101 ++++++++- src/halmos/bitvec.py | 5 +- src/halmos/build.py | 298 ++++++++++++++++++++++++-- src/halmos/cheatcodes.py | 15 +- src/halmos/config.py | 7 +- src/halmos/contract.py | 3 +- src/halmos/mapper.py | 16 ++ src/halmos/sevm.py | 11 +- src/halmos/utils.py | 3 +- tests/expected/all.json | 280 ++++++++++++++++++++++++ tests/expected/vyper.json | 108 ++++++++++ tests/test_config.py | 18 +- tests/test_halmos.py | 17 +- tests/test_vyper.py | 330 +++++++++++++++++++++++++++++ tests/vyper/foundry.toml | 11 + tests/vyper/remappings.txt | 3 + tests/vyper/src/Counter.vy | 15 ++ tests/vyper/src/Immutable.vy | 14 ++ tests/vyper/src/Vault.vy | 21 ++ tests/vyper/test/CounterTest.vy | 71 +++++++ tests/vyper/test/VyperTarget.t.sol | 70 ++++++ 22 files changed, 1404 insertions(+), 41 deletions(-) create mode 100644 tests/expected/vyper.json create mode 100644 tests/test_vyper.py create mode 100644 tests/vyper/foundry.toml create mode 100644 tests/vyper/remappings.txt create mode 100644 tests/vyper/src/Counter.vy create mode 100644 tests/vyper/src/Immutable.vy create mode 100644 tests/vyper/src/Vault.vy create mode 100644 tests/vyper/test/CounterTest.vy create mode 100644 tests/vyper/test/VyperTarget.t.sol diff --git a/README.md b/README.md index b307c48e..8b72ebe5 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,31 @@ prediction then agree. **Fusaka / Cancun opcodes.** `CLZ` (EIP-7939), with a concrete fast path and a logarithmic symbolic encoding, plus `BLOBHASH` and `BLOBBASEFEE` (EIP-4844). +**Vyper.** Test contracts can be written in Vyper, and Solidity tests can target +Vyper contracts. Upstream only reads the `.sol` directories of the build output, +and forge's Vyper artifacts have no AST, no metadata and an empty +`methodIdentifiers`; dolmos reads `.vy`/`.vyi` artifacts too, computes the +selectors from the ABI, takes the source path and compiler version from forge's +cache, and makes Vyper contracts visible to Solidity tests (deployed code, ABI +for invariant targets and traces, including contracts with immutables). +`vm.getCode` accepts `Foo.vy`, `src/Foo.vy` and `Foo.vy:Foo`. + +A Vyper test looks like a Solidity one: `setUp()` and `check_*`/`invariant_*` +functions, with cheatcodes called through an interface at +`0x7109709ECfa91a80626fF3989D68f67F5b1DD12D`. Use `assert ..., UNREACHABLE` for +properties: it compiles to the INVALID opcode, which the new +`--invalid-as-failure` option reports as a failure, even when it is hit in a +nested call and bubbles up as an empty revert. A plain `assert` is an empty +revert, indistinguishable from a `require`, so the path is silently discarded. +The `vm.assert*` cheatcodes work as well. `@custom:halmos` annotations go in the +module and function docstrings. + +When the build contains Vyper contracts, `--storage-layout generic` (Vyper +computes `HashMap` slots as `keccak256(slot . key)`) and `--invalid-as-failure` +become the defaults; a value set in `dolmos.toml`, an annotation or on the +command line still wins. Coverage reports do not include Vyper code, since forge +emits no source maps for it. See `tests/vyper` for examples. + **CLI.** The command is `dolmos`; `halmos` is no longer installed. A project config file may be named `dolmos.toml` (read before `halmos.toml`), `--help` and `--version` report the name the command was invoked with, and `-test` is a short @@ -120,6 +145,9 @@ the CLZ tests, and `test/OpCodesCLZ.sol` needs a solc recent enough to accept And `test/Invalid.t.sol`, inherited from upstream, requires solc `^0.5.2`; if that version is unavailable, `forge build` stops there. +The Vyper tests live in `tests/vyper` and need the `vyper` compiler in `PATH` +(`pip install vyper`); pytest skips them otherwise. + ## Contributing Fixes that are not specific to Dowsers should go upstream: branch off diff --git a/src/halmos/__main__.py b/src/halmos/__main__.py index 2e5de109..d7689389 100644 --- a/src/halmos/__main__.py +++ b/src/halmos/__main__.py @@ -42,6 +42,8 @@ import halmos.traces from halmos.build import ( build_output_iterator, + get_source_path, + has_vyper_contracts, import_libs, parse_build_out, parse_devdoc, @@ -66,7 +68,12 @@ ) from halmos.contract import CoverageReporter from halmos.env import init_env -from halmos.exceptions import FailCheatcode, HalmosException +from halmos.exceptions import ( + FailCheatcode, + HalmosException, + InvalidOpcode, + Revert, +) from halmos.flamegraphs import CallSequenceFlamegraph, call_flamegraph, exec_flamegraph from halmos.logs import ( COUNTEREXAMPLE_INVALID, @@ -209,6 +216,40 @@ def with_natspec( return args.with_overrides(source, **vars(overrides)) +def with_vyper_defaults(args: HalmosConfig) -> HalmosConfig: + """ + Adjust the defaults for projects that contain vyper contracts: + - the solidity storage layout model does not apply to vyper (e.g. HashMap slots + are computed as keccak256(slot . key) instead of keccak256(key . slot)) + - vyper uses the INVALID opcode for `assert ..., UNREACHABLE`, its equivalent of + solidity's `assert` (Panic(0x01)) + + Values set in a config file, an annotation or on the command line take precedence. + """ + vyper_defaults = {"storage_layout": "generic", "invalid_as_failure": True} + + overrides = { + name: value + for name, value in vyper_defaults.items() + if args.value_with_source(name)[1] <= ConfigSource.default + } + + if not overrides: + return args + + print( + "Vyper contracts found, using " + + ", ".join( + f"--{name.replace('_', '-')}" + ("" if value is True else f" {value}") + for name, value in overrides.items() + ) + + " by default" + ) + + # same precedence as the global defaults, so any explicit setting still wins + return args.with_overrides(ConfigSource.default, **overrides) + + def load_config(_args) -> HalmosConfig: config = default_config() @@ -450,6 +491,30 @@ def is_global_fail_set(context: CallContext) -> bool: return hevm_fail or any(is_global_fail_set(x) for x in context.subcalls()) +def is_invalid_opcode_found(context: CallContext) -> bool: + """ + Check if the call failed because of an INVALID opcode (0xFE), e.g. vyper's + `assert ..., UNREACHABLE`. + + The INVALID opcode may have been hit in a nested call: the callers then see a + failed call with no returndata and typically bubble it up as an empty revert + (vyper `extcall`/`staticcall`, solidity high-level calls), so we follow the chain + of empty reverts down the last subcall. + """ + output = context.output + if isinstance(output.error, InvalidOpcode): + return True + + if not isinstance(output.error, Revert): + return False + + if output.data is not None and len(output.data) != 0: + return False + + last_subcall = context.last_subcall() + return last_subcall is not None and is_invalid_opcode_found(last_subcall) + + def get_state_id(ex: Exec) -> bytes: """ Computes the state snapshot hash, incorporating constraints on state variables. @@ -682,7 +747,16 @@ def _compute_frontier(ctx: ContractContext, depth: int) -> Iterator[Exec]: if subcall.output.error: # ignore normal reverts panic_found = post_ex.is_panic_of(panic_error_codes) - if not panic_found and not is_global_fail_set(subcall): + invalid_found = ( + not panic_found + and args.invalid_as_failure + and is_invalid_opcode_found(subcall) + ) + if ( + not panic_found + and not invalid_found + and not is_global_fail_set(subcall) + ): continue fun_info = subcall.message.fun_info @@ -699,6 +773,7 @@ def _compute_frontier(ctx: ContractContext, depth: int) -> Iterator[Exec]: ex=post_ex, panic_found=panic_found, description=msg, + invalid_found=invalid_found, ) except ShutdownError: if args.debug: @@ -807,6 +882,7 @@ def handle_assertion_violation( ex: Exec, panic_found: bool, description: str = None, + invalid_found: bool = False, ) -> None: """ Handles a potential assertion violation by solving it in a separate process. @@ -819,6 +895,7 @@ def handle_assertion_violation( ex: The execution state containing the potential violation panic_found: Whether it's a panic error or a legacy hevm.fail flag description: Optional description of the violation + invalid_found: Whether an INVALID opcode was hit (see --invalid-as-failure) Raises: ShutdownError: If the executor has been shutdown during the solving process @@ -833,6 +910,8 @@ def handle_assertion_violation( if panic_found: panic_code = unbox_int(output.data[4:36].unwrap()) print(f"Panic(0x{panic_code:02x}) {error_output}") + elif invalid_found: + print(f"INVALID opcode (0xFE) {error_output}") else: # fail_found print(f"(fail flag set) {error_output}") @@ -1090,8 +1169,13 @@ def run_test(ctx: FunctionContext) -> TestResult: output = ex.context.output error_output = output.error panic_found = ex.is_panic_of(args.panic_error_codes) + invalid_found = ( + not panic_found + and args.invalid_as_failure + and is_invalid_opcode_found(ex.context) + ) - if panic_found or is_global_fail_set(ex.context): + if panic_found or invalid_found or is_global_fail_set(ex.context): potential += 1 try: @@ -1099,6 +1183,7 @@ def run_test(ctx: FunctionContext) -> TestResult: path_id=path_id, ex=ex, panic_found=panic_found, + invalid_found=invalid_found, ) except ShutdownError: if args.debug: @@ -1733,7 +1818,9 @@ def _main(_args=None) -> MainResult: if args.version: # the distribution is dolmos, the importable package is still halmos - print(f"{os.path.basename(sys.argv[0]) or 'dolmos'} {metadata.version('dolmos')}") + print( + f"{os.path.basename(sys.argv[0]) or 'dolmos'} {metadata.version('dolmos')}" + ) return MainResult(0) init_env(args.root) @@ -1802,6 +1889,10 @@ def _main(_args=None) -> MainResult: traceback.print_exc() return MainResult(1) + # vyper support: switch some defaults, unless explicitly configured + if has_vyper_contracts(build_out): + args = with_vyper_defaults(args) + timer.create_subtimer("tests") total_passed = 0 @@ -1866,7 +1957,7 @@ def on_signal(signum, frame): linkReferences = contract_json["bytecode"]["linkReferences"] libs = import_libs(build_out_map, creation_hexcode, linkReferences) - contract_path = f"{contract_json['ast']['absolutePath']}:{contract_name}" + contract_path = f"{get_source_path(contract_json, filename)}:{contract_name}" print(f"\nRunning {num_found} tests for {contract_path}") # Set the test contract address in DeployAddressMapper diff --git a/src/halmos/bitvec.py b/src/halmos/bitvec.py index 4856e331..1c0f4a6d 100644 --- a/src/halmos/bitvec.py +++ b/src/halmos/bitvec.py @@ -1,4 +1,3 @@ - # SPDX-License-Identifier: AGPL-3.0 from typing import Any, TypeAlias @@ -881,7 +880,7 @@ def clz(self) -> "HalmosBitVec": acc = If(top_zero, acc + BitVecVal(shift, size), acc) cur = If( top_zero, - cur << BitVecVal(shift, size), # expose next band at MSB + cur << BitVecVal(shift, size), # expose next band at MSB top_bits << BitVecVal(size - shift, size), # keep current band aligned ) @@ -1007,4 +1006,4 @@ def byte(self, idx: int, *, output_size: int = 8) -> BV: FALSE.sym_val = None ZERO = HalmosBitVec(0, size=256) -ONE = HalmosBitVec(1, size=256) \ No newline at end of file +ONE = HalmosBitVec(1, size=256) diff --git a/src/halmos/build.py b/src/halmos/build.py index d0457b2e..ad6d5ea0 100644 --- a/src/halmos/build.py +++ b/src/halmos/build.py @@ -5,11 +5,187 @@ import re import traceback +from eth_hash.auto import keccak + +from halmos.calldata import str_abi from halmos.config import Config as HalmosConfig from halmos.logs import PARSING_ERROR, debug, warn_code from halmos.mapper import Mapper, SourceFileMap from halmos.ui import ui +SOLIDITY_EXTENSIONS = (".sol",) +VYPER_EXTENSIONS = (".vy", ".vyi") + +# build output groups are keyed by compiler version; vyper groups get this prefix +# so that they never collide with solc versions (e.g. vyper 0.4.3 vs solc 0.4.3) +VYPER_VERSION_PREFIX = "vyper:" + +FORGE_CACHE_FILE = os.path.join("cache", "solidity-files-cache.json") + + +def is_vyper_file(filename: str) -> bool: + return filename.endswith(VYPER_EXTENSIONS) + + +def is_vyper_group(compiler_version: str) -> bool: + return compiler_version.startswith(VYPER_VERSION_PREFIX) + + +def is_vyper_contract(contract_json: dict) -> bool: + return contract_json.get("language") == "vyper" + + +def get_source_path(contract_json: dict, default: str | None = None) -> str | None: + """ + Return the source path of a build artifact (e.g. `test/Foo.t.sol`). + + solc artifacts carry it in their AST; vyper artifacts get it from the forge + cache during normalization (see `normalize_vyper_artifact`). + """ + if path := contract_json.get("sourcePath"): + return path + try: + return contract_json["ast"]["absolutePath"] + except (KeyError, TypeError): + return default + + +def load_forge_cache(root: str, out_path: str) -> dict: + """ + Return a mapping from artifact path (relative to the build output directory, + e.g. `Counter.vy/Counter.json`) to a (source path, compiler version) pair. + + Vyper artifacts emitted by forge only contain the abi and the bytecode, so the + forge cache is the only place where the source path and compiler version live. + Best effort: returns an empty mapping if the cache is missing or unreadable. + """ + cache_path = os.path.join(root, FORGE_CACHE_FILE) + result = {} + + try: + with open(cache_path, encoding="utf8") as f: + cache = json.load(f) + except (OSError, ValueError) as err: + debug(f"could not read forge cache {cache_path}: {err}") + return result + + for source_path, entry in cache.get("files", {}).items(): + for versions in entry.get("artifacts", {}).values(): + for version, profiles in versions.items(): + for artifact in profiles.values(): + artifact_path = artifact.get("path") + if not artifact_path: + continue + # paths are relative to the out dir; tolerate an `out/` prefix + artifact_path = os.path.normpath(artifact_path) + out_prefix = os.path.basename(os.path.normpath(out_path)) + os.sep + if artifact_path.startswith(out_prefix): + artifact_path = artifact_path[len(out_prefix) :] + result[os.path.normpath(artifact_path)] = (source_path, version) + + return result + + +_DOCSTRING = r'(?:"""(?P.*?)"""|\'\'\'(?P.*?)\'\'\')' + +# module docstring: the first string literal of the file, possibly after comments +# such as `# pragma version ...` +_VYPER_MODULE_DOCSTRING = re.compile(r"\A(?:\s*#[^\n]*\n|\s+)*" + _DOCSTRING, re.DOTALL) + +# function docstring: first string literal right after the (possibly multiline) signature +_VYPER_FUNCTION_DOCSTRING = re.compile( + r"^[ \t]*def[ \t]+(?P\w+)[ \t]*\((?:[^()]|\([^()]*\))*\)" + r"(?:[ \t]*->[^:\n]+)?[ \t]*:[ \t]*(?:#[^\n]*)?\n\s*" + _DOCSTRING, + re.DOTALL | re.MULTILINE, +) + + +def _docstring_text(match: re.Match) -> str: + return match.group("doc") if match.group("doc") is not None else match.group("doc2") + + +def parse_vyper_docstrings(source: str) -> tuple[str | None, dict[str, str]]: + """ + Extract the module docstring and the per-function docstrings of a vyper source. + + Returns (module docstring or None, function name -> docstring). + """ + module_doc = None + if match := _VYPER_MODULE_DOCSTRING.match(source): + module_doc = _docstring_text(match) + + function_docs = {} + for match in _VYPER_FUNCTION_DOCSTRING.finditer(source): + function_docs.setdefault(match.group("name"), _docstring_text(match)) + + return module_doc, function_docs + + +def normalize_vyper_artifact( + json_out: dict, dirname: str, root: str, cache_entry +) -> tuple[str, str, dict | None]: + """ + Bring a forge vyper artifact into the shape that halmos expects from solc artifacts. + + forge only emits `abi`, `bytecode`, `deployedBytecode`, an empty `methodIdentifiers` + and an `id` for vyper, so we fill in: + - `methodIdentifiers`, computed from the abi + - `bytecode.linkReferences` (vyper has no libraries) + - `metadata.compiler.version` and `metadata.output.devdoc` (for `@custom:halmos`) + - `sourcePath` and `language` + + Returns (contract type, compiler version group key, natspec). + """ + source_path, version = cache_entry if cache_entry else (dirname, "unknown") + + json_out["language"] = "vyper" + json_out["sourcePath"] = source_path + + if not json_out.get("methodIdentifiers"): + json_out["methodIdentifiers"] = { + (sig := str_abi(item)): keccak(sig.encode()).hex()[:8] + for item in json_out.get("abi", []) + if item.get("type") == "function" + } + + for key in ("bytecode", "deployedBytecode"): + entry = json_out.get(key) + if isinstance(entry, str): + entry = {"object": entry} + entry = entry or {"object": "0x"} + entry.setdefault("object", "0x") + entry.setdefault("linkReferences", {}) + json_out[key] = entry + + # vyper modules are always contracts, .vyi files are always interfaces + contract_type = "interface" if dirname.endswith(".vyi") else "contract" + + # `@custom:halmos` annotations live in docstrings; forge does not give us the + # devdoc for vyper, so read them straight from the source + natspec = None + devdoc_methods = {} + try: + with open(os.path.join(root, source_path), encoding="utf8") as f: + module_doc, function_docs = parse_vyper_docstrings(f.read()) + + if module_doc: + natspec = {"text": module_doc} + + for funsig in json_out["methodIdentifiers"]: + doc = function_docs.get(funsig.split("(")[0]) + if doc and (halmos_args := parse_natspec({"text": doc})): + devdoc_methods[funsig] = {"custom:halmos": halmos_args} + except OSError as err: + debug(f"could not read vyper source {source_path}: {err}") + + metadata = json_out.setdefault("metadata", {}) + metadata.setdefault("compiler", {}).setdefault("version", version) + metadata.setdefault("output", {}).setdefault("devdoc", {}).setdefault( + "methods", {} + ).update(devdoc_methods) + + return contract_type, VYPER_VERSION_PREFIX + version, natspec + def get_contract_type( ast_nodes: list, contract_name: str @@ -40,8 +216,12 @@ def parse_build_out(args: HalmosConfig) -> dict: ) ui.update_status(f"Parsing {out_path}") + + # only needed for vyper artifacts, loaded lazily + forge_cache = None + for sol_dirname in os.listdir(out_path): # for each source filename - if not sol_dirname.endswith(".sol"): + if not sol_dirname.endswith(SOLIDITY_EXTENSIONS + VYPER_EXTENSIONS): continue sol_path = os.path.join(out_path, sol_dirname) @@ -59,14 +239,40 @@ def parse_build_out(args: HalmosConfig) -> dict: with open(json_path, encoding="utf8") as f: json_out = json.load(f) + # cut off compiler version number as well + contract_name = json_filename.split(".")[0] + + if is_vyper_file(sol_dirname): + if forge_cache is None: + forge_cache = load_forge_cache(root, out_path) + + cache_entry = forge_cache.get( + os.path.normpath(os.path.join(sol_dirname, json_filename)) + ) + contract_type, compiler_version, natspec = normalize_vyper_artifact( + json_out, sol_dirname, root, cache_entry + ) + + # note: coverage is not supported for vyper (no source maps), + # and vyper file ids would clash with solc ones + add_to_build_out( + result, + compiler_version, + sol_dirname, + contract_name, + (json_out, contract_type, natspec), + ) + parse_symbols( + args, result[compiler_version][sol_dirname], contract_name + ) + continue + ast = json_out["ast"] if coverage_output: # record the mapping between file id and file path SourceFileMap().add_mapping(json_out["id"], ast["absolutePath"]) - # cut off compiler version number as well - contract_name = json_filename.split(".")[0] ast_nodes = ast["nodes"] contract_type, natspec = get_contract_type(ast_nodes, contract_name) @@ -80,19 +286,16 @@ def parse_build_out(args: HalmosConfig) -> dict: continue compiler_version = json_out["metadata"]["compiler"]["version"] - result.setdefault(compiler_version, {}) - result[compiler_version].setdefault(sol_dirname, {}) - contract_map = result[compiler_version][sol_dirname] - - 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) - parse_symbols(args, contract_map, contract_name) + add_to_build_out( + result, + compiler_version, + sol_dirname, + contract_name, + (json_out, contract_type, natspec), + ) + parse_symbols( + args, result[compiler_version][sol_dirname], contract_name + ) except Exception as err: warn_code( @@ -107,6 +310,21 @@ def parse_build_out(args: HalmosConfig) -> dict: return result +def add_to_build_out( + result: dict, compiler_version: str, filename: str, contract_name: str, entry +) -> None: + contract_map = result.setdefault(compiler_version, {}).setdefault(filename, {}) + + if contract_name in contract_map: + raise ValueError( + "duplicate contract names in the same file", + contract_name, + filename, + ) + + contract_map[contract_name] = entry + + def parse_symbols(args: HalmosConfig, contract_map: dict, contract_name: str) -> None: try: json_out = contract_map[contract_name][0] @@ -117,7 +335,9 @@ def parse_symbols(args: HalmosConfig, contract_map: dict, contract_name: str) -> contract_mapping_info = Mapper().get_or_create(contract_name) contract_mapping_info.bytecode = bytecode - Mapper().parse_ast(json_out["ast"]) + # vyper artifacts have no (solc-style) ast + if "ast" in json_out: + Mapper().parse_ast(json_out["ast"]) except Exception: debug(f"error parsing symbols for contract {contract_name}") @@ -186,9 +406,51 @@ def import_libs(build_out_map: dict, hexcode: str, linkReferences: dict) -> dict return libs +def has_vyper_contracts(build_out: dict) -> bool: + return any(is_vyper_group(version) for version in build_out) + + +def build_out_view(build_out: dict, compiler_version: str) -> dict: + """ + Return the build output map visible to test contracts of the given group. + + Test contracts normally only see contracts compiled with the same compiler version + (e.g. for library linking and bytecode lookups). Cross-language setups are common + though (solidity tests for vyper contracts, and vice versa), so: + - solidity groups also see every vyper contract + - vyper groups see every vyper contract, and the solidity contracts if there is + a single solc version (otherwise the lookup would be ambiguous) + + Solidity and vyper filenames can't collide since they have different extensions. + """ + own = build_out[compiler_version] + vyper_groups = [v for v in build_out if is_vyper_group(v)] + solc_groups = [v for v in build_out if not is_vyper_group(v)] + + if is_vyper_group(compiler_version): + shared = [v for v in vyper_groups if v != compiler_version] + if len(solc_groups) == 1: + shared += solc_groups + else: + shared = vyper_groups + + if not shared: + return own + + view = dict(own) + for version in sorted(shared): + for filename, contracts in build_out[version].items(): + view.setdefault(filename, contracts) + return view + + def build_output_iterator(build_out: dict): for compiler_version in sorted(build_out): build_out_map = build_out[compiler_version] + + # computed once per group, so that the BuildOut singleton can cache it + view = build_out_view(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) + yield (view, filename, contract_name) diff --git a/src/halmos/cheatcodes.py b/src/halmos/cheatcodes.py index 39bfae32..87d4a565 100644 --- a/src/halmos/cheatcodes.py +++ b/src/halmos/cheatcodes.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: AGPL-3.0 import json +import os import re from contextlib import suppress from dataclasses import dataclass @@ -1149,9 +1150,17 @@ def handle(sevm, ex, arg: ByteVec, stack) -> ByteVec | None: path_len = arg.get_word(36) path = arg[68 : 68 + path_len].unwrap().decode("utf-8") + out_dir = sevm.options.forge_build_out if ":" in path: + # `File.sol:Name`, `path/to/File.vy:Name` [filename, contract_name] = path.split(":") - path = "out/" + filename + "/" + contract_name + ".json" + filename = os.path.basename(filename) + path = f"{out_dir}/{filename}/{contract_name}.json" + elif path.endswith((".sol", ".vy")): + # `File.sol`, `path/to/File.vy`: the contract named after the file + filename = os.path.basename(path) + contract_name = filename.rsplit(".", 1)[0] + path = f"{out_dir}/{filename}/{contract_name}.json" target = sevm.options.root.rstrip("/") path = target + "/" + path @@ -1244,7 +1253,9 @@ def handle(sevm, ex, arg: ByteVec, stack) -> ByteVec | None: nonce = int_of(arg.get_word(36), f"symbolic nonce in vm.{name}()") if not (0 <= nonce < 2**64): - raise HalmosException(f"vm.{name}(): nonce out of uint64 range: {nonce}") + raise HalmosException( + f"vm.{name}(): nonce out of uint64 range: {nonce}" + ) who = who.as_z3() current = ex.nonce_of(who) diff --git a/src/halmos/config.py b/src/halmos/config.py index ce0cd5cb..0ed9c010 100644 --- a/src/halmos/config.py +++ b/src/halmos/config.py @@ -299,6 +299,11 @@ class Config: action=ParseErrorCodes, ) + invalid_as_failure: bool = arg( + help="treat the INVALID opcode (0xFE) as a test failure, like a Panic(0x01) revert. This is how Vyper compiles `assert ..., UNREACHABLE` (and how solc < 0.8 compiles `assert`). Enabled by default when the build output contains Vyper contracts.", + global_default=False, + ) + invariant_depth: int = arg( help="set depth for invariant testing (length of the stateful sequence of calls)", global_default=2, @@ -345,7 +350,7 @@ class Config: ) storage_layout: str = arg( - 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.", + 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. Defaults to generic when the build output contains Vyper contracts.", global_default="solidity", choices=["solidity", "generic"], ) diff --git a/src/halmos/contract.py b/src/halmos/contract.py index 0146e216..d1b8a268 100644 --- a/src/halmos/contract.py +++ b/src/halmos/contract.py @@ -1,4 +1,3 @@ - # SPDX-License-Identifier: AGPL-3.0 from collections import defaultdict @@ -540,4 +539,4 @@ def generate_lcov_report(self) -> str: # End of file lcov_lines.append("end_of_record") - return "\n".join(lcov_lines) \ No newline at end of file + return "\n".join(lcov_lines) diff --git a/src/halmos/mapper.py b/src/halmos/mapper.py index f534e0c6..562c6aec 100644 --- a/src/halmos/mapper.py +++ b/src/halmos/mapper.py @@ -204,6 +204,7 @@ def __init__(self): self._build_out_map: dict = None self._build_out_map_reverse: dict = None self._build_out_map_code: dict = None + self._vyper_code: list = None def set_build_out(self, build_out_map: dict): if self._build_out_map is build_out_map: @@ -212,6 +213,7 @@ def set_build_out(self, build_out_map: dict): self._build_out_map = build_out_map self._build_out_map_reverse = None self._build_out_map_code = None + self._vyper_code = None def create_build_out_map_reverse(self): # create reverse mapping @@ -276,6 +278,10 @@ def create_build_out_map_code(self): """ self._build_out_map_code = defaultdict(list) + # vyper appends immutables to the runtime code at deployment time, so the + # deployed code is longer than the compile-time runtime code (matched by prefix) + self._vyper_code = [] + empty = {"object": "0x"} for filename, file_map in self._build_out_map.items(): for contract_name, (contract_map, _, _) in file_map.items(): @@ -292,6 +298,9 @@ def create_build_out_map_code(self): code_data = (hexcode, placeholders, contract_name, filename, source_map) self._build_out_map_code[size].append(code_data) + if contract_map.get("language") == "vyper": + self._vyper_code.append(code_data) + def get_by_code( self, bytecode: ForwardRef("ByteVec") ) -> tuple[str | None, str | None, str | None]: @@ -323,6 +332,13 @@ def eq_bytes(bytecode: ForwardRef("ByteVec"), hexcode: str): if eq_except_placeholders(hexcode, placeholders): return (contract_name, filename, source_map) + # vyper contracts with immutables: compile-time runtime code + immutables section + for code_data in self._vyper_code: + hexcode, _, contract_name, filename, source_map = code_data + size = len(hexcode) // 2 + if size < len(bytecode) and eq_bytes(bytecode[:size], hexcode): + return (contract_name, filename, source_map) + return (None, None, None) def get_by_name(self, contract_name: str, filename: str = None) -> dict: diff --git a/src/halmos/sevm.py b/src/halmos/sevm.py index 47daf4b2..32e6b1c6 100644 --- a/src/halmos/sevm.py +++ b/src/halmos/sevm.py @@ -1,4 +1,3 @@ - # SPDX-License-Identifier: AGPL-3.0 import itertools @@ -73,8 +72,8 @@ OP_AND, OP_BALANCE, OP_BASEFEE, - OP_BLOBHASH, OP_BLOBBASEFEE, + OP_BLOBHASH, OP_BLOCKHASH, OP_BYTE, OP_CALL, @@ -84,6 +83,7 @@ OP_CALLER, OP_CALLVALUE, OP_CHAINID, + OP_CLZ, OP_CODECOPY, OP_CODESIZE, OP_COINBASE, @@ -134,7 +134,6 @@ OP_RETURNDATASIZE, OP_REVERT, OP_SAR, - OP_CLZ, OP_SDIV, OP_SELFBALANCE, OP_SGT, @@ -775,7 +774,7 @@ class Block: number: BitVecRef timestamp: BitVecRef blobbasefee: BitVecRef # EIP-4844 / Cancun — opcode 0x4A - blobhashes: list # EIP-4844 / Cancun — opcode 0x49, list of BitVecRef (256-bit) + blobhashes: list # EIP-4844 / Cancun — opcode 0x49, list of BitVecRef (256-bit) def __init__(self, **kwargs) -> None: self.basefee = kwargs["basefee"] @@ -786,7 +785,7 @@ def __init__(self, **kwargs) -> None: self.number = kwargs["number"] self.timestamp = kwargs["timestamp"] self.blobbasefee = kwargs.get("blobbasefee", ZERO) # 0 par défaut - self.blobhashes = kwargs.get("blobhashes", []) # pas de blobs par défaut + self.blobhashes = kwargs.get("blobhashes", []) # pas de blobs par défaut assert_address(self.coinbase) @@ -3853,4 +3852,4 @@ def get_top_instructions(self, n: int = 20) -> list[tuple[str, int]]: return [(key, count) for key, count in self.counters.most_common(n)] def reset(self) -> None: - raise NotImplementedError("Resetting the profiler is not supported") \ No newline at end of file + raise NotImplementedError("Resetting the profiler is not supported") diff --git a/src/halmos/utils.py b/src/halmos/utils.py index 63691f90..403b23a1 100644 --- a/src/halmos/utils.py +++ b/src/halmos/utils.py @@ -1,4 +1,3 @@ - # SPDX-License-Identifier: AGPL-3.0 from __future__ import annotations @@ -1721,4 +1720,4 @@ def wrapper(*args, **kwargs): 0xF1AFE04D: "removeFile(string)", 0x897E0A97: "writeFile(string,string)", 0x619D897F: "writeLine(string,string)", -} \ No newline at end of file +} diff --git a/tests/expected/all.json b/tests/expected/all.json index b9cb7594..50fd3ab3 100644 --- a/tests/expected/all.json +++ b/tests/expected/all.json @@ -858,6 +858,82 @@ "num_bounded_loops": null } ], + "test/Create3.t.sol:Create3Test": [ + { + "name": "check_create3(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_create_address_prediction()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_create_address_prediction_nonce2()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/Create3.t.sol:NonceTest": [ + { + "name": "check_nonce_of_created_contract()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_nonce_of_test_contract()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_resetNonce_of_contract()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_setNonce()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_setNonce_changes_create_address()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], "test/Deal.t.sol:DealTest": [ { "name": "check_concretized_transfer_over_max_eth(bytes)", @@ -4020,6 +4096,210 @@ "num_bounded_loops": null } ], + "test/OpCodesBlobBaseFee_BlobHash.t.sol:BlobBaseFeeTest": [ + { + "name": "check_blobbasefee_affordable(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_blobbasefee_no_revert()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_blobbasefee_solidity_matches_opcode()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_blobbasefee_zero_in_test_env()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/OpCodesBlobBaseFee_BlobHash.t.sol:BlobHashTest": [ + { + "name": "check_blobhash_contiguous(uint8)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_blobhash_deterministic(uint8)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_blobhash_no_revert(uint8)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_blobhash_out_of_range()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_blobhash_version_byte(uint8)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_blobhash_zero_no_blob_tx()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/OpCodesCLZ.sol:ClzTest": [ + { + "name": "check_clz_byte()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_clz_lower_bound(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_clz_max()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_clz_monotone(uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_clz_msb()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_clz_one()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_clz_power_of_two(uint8)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_clz_range(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_clz_two()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_clz_upper_bound(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_clz_zero()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_clz_zero_symbolic()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], "test/Opcode.t.sol:OpcodeTest": [ { "name": "check_PUSH0()", diff --git a/tests/expected/vyper.json b/tests/expected/vyper.json new file mode 100644 index 00000000..45e2b2e7 --- /dev/null +++ b/tests/expected/vyper.json @@ -0,0 +1,108 @@ +{ + "exitcode": 1, + "test_results": { + "test/VyperTarget.t.sol:VyperTargetTest": [ + { + "name": "check_counter(uint256)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_hashmap(address,address,uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": [], + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_immutable(uint256)", + "exitcode": 0, + "num_models": 0, + "models": [], + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_unreachable_caught(uint256)", + "exitcode": 0, + "num_models": 0, + "models": [], + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_unreachable_in_target(uint256)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/CounterTest.vy:CounterTest": [ + { + "name": "check_add_cheatcode(uint256)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_add_ok(uint256)", + "exitcode": 0, + "num_models": 0, + "models": [], + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_add_unreachable(uint256)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_bytes_annotated(bytes)", + "exitcode": 0, + "num_models": 0, + "models": [], + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_bytes_default(bytes)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_plain_assert(uint256)", + "exitcode": 0, + "num_models": 0, + "models": [], + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ] + } +} diff --git a/tests/test_config.py b/tests/test_config.py index 1ada79fc..ac4a8f63 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -131,7 +131,23 @@ def test_config_file_in_project_root(): config_files = resolve_config_files(args, include_missing=True) # then the config file should be in the project root - assert config_files == [os.path.join(base_path, "halmos.toml")] + # (dolmos.toml is the preferred name when neither file exists) + assert config_files == [os.path.join(base_path, "dolmos.toml")] + + +def test_config_file_halmos_toml_fallback(tmp_path): + # halmos.toml is still picked up when there is no dolmos.toml + (tmp_path / "halmos.toml").write_text("[global]\n") + config_files = resolve_config_files(["--root", str(tmp_path)]) + assert config_files == [str(tmp_path / "halmos.toml")] + + +def test_config_file_dolmos_toml_first(tmp_path): + # dolmos.toml takes precedence when both exist + (tmp_path / "halmos.toml").write_text("[global]\n") + (tmp_path / "dolmos.toml").write_text("[global]\n") + config_files = resolve_config_files(["--root", str(tmp_path)]) + assert config_files == [str(tmp_path / "dolmos.toml")] def test_config_file_explicit(): diff --git a/tests/test_halmos.py b/tests/test_halmos.py index d5995e3f..d43ecff3 100644 --- a/tests/test_halmos.py +++ b/tests/test_halmos.py @@ -1,5 +1,6 @@ import dataclasses import json +import shutil import pytest @@ -17,7 +18,7 @@ "tests/expected/all.json", ), ( - ["--root", "tests/ffi"], + ["--root", "tests/ffi", "--ffi"], "tests/expected/ffi.json", ), ( @@ -59,6 +60,20 @@ def test_main(cmd, expected_path, halmos_options): assert_eq(expected["test_results"], actual["test_results"]) +@pytest.mark.skipif( + shutil.which("vyper") is None, reason="vyper compiler not found in PATH" +) +def test_main_vyper(halmos_options): + # vyper tests and vyper targets (requires the vyper compiler, used by forge) + actual = dataclasses.asdict( + _main(["--root", "tests/vyper"] + halmos_options.split()) + ) + with open("tests/expected/vyper.json", 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", [ diff --git a/tests/test_vyper.py b/tests/test_vyper.py new file mode 100644 index 00000000..98bf52c1 --- /dev/null +++ b/tests/test_vyper.py @@ -0,0 +1,330 @@ +import json + +import pytest + +from halmos.__main__ import is_invalid_opcode_found, load_config, with_vyper_defaults +from halmos.build import ( + VYPER_VERSION_PREFIX, + build_out_view, + build_output_iterator, + get_source_path, + has_vyper_contracts, + load_forge_cache, + normalize_vyper_artifact, + parse_build_out, + parse_devdoc, + parse_natspec, + parse_vyper_docstrings, +) +from halmos.bytevec import ByteVec +from halmos.config import ConfigSource +from halmos.exceptions import InvalidOpcode, Revert + +VYPER_SOURCE = '''# pragma version ~=0.4.3 +""" +@title Test +@custom:halmos --loop 4 +""" + +counter: uint256 + + +@external +def check_one(x: uint256): + """ + @notice first + @custom:halmos --default-bytes-lengths 0,10 + """ + assert x != 1, UNREACHABLE + + +@external +def check_multiline( + x: uint256, + y: DynArray[uint256, 3], +) -> uint256: # trailing comment + \'\'\' + @custom:halmos --loop 5 + \'\'\' + return x + + +@external +def no_doc(x: uint256): + pass +''' + +ABI = [ + { + "type": "function", + "name": "check_one", + "inputs": [{"name": "x", "type": "uint256"}], + "outputs": [], + "stateMutability": "nonpayable", + }, + { + "type": "function", + "name": "check_multiline", + "inputs": [ + {"name": "x", "type": "uint256"}, + {"name": "y", "type": "uint256[]"}, + ], + "outputs": [{"name": "", "type": "uint256"}], + "stateMutability": "nonpayable", + }, + { + "type": "function", + "name": "no_doc", + "inputs": [{"name": "x", "type": "uint256"}], + "outputs": [], + "stateMutability": "nonpayable", + }, +] + + +def forge_vyper_artifact(): + # the shape of the artifacts emitted by forge for vyper + return { + "abi": ABI, + "bytecode": {"object": "0x6000"}, + "deployedBytecode": {"object": "0x5f5ffd"}, + "methodIdentifiers": {}, + "id": 1, + } + + +def test_parse_vyper_docstrings(): + module_doc, function_docs = parse_vyper_docstrings(VYPER_SOURCE) + + assert parse_natspec({"text": module_doc}) == "--loop 4" + assert set(function_docs) == {"check_one", "check_multiline"} + assert ( + parse_natspec({"text": function_docs["check_one"]}) + == "--default-bytes-lengths 0,10" + ) + assert parse_natspec({"text": function_docs["check_multiline"]}) == "--loop 5" + + +def test_parse_vyper_docstrings_none(): + assert parse_vyper_docstrings("x: uint256\n") == (None, {}) + + +@pytest.fixture +def vyper_project(tmp_path): + (tmp_path / "test").mkdir() + (tmp_path / "test" / "Foo.vy").write_text(VYPER_SOURCE) + + out = tmp_path / "out" + (out / "Foo.vy").mkdir(parents=True) + (out / "Foo.vy" / "Foo.json").write_text(json.dumps(forge_vyper_artifact())) + + (tmp_path / "cache").mkdir() + cache = { + "files": { + "test/Foo.vy": { + "sourceName": "test/Foo.vy", + "artifacts": { + "Foo": { + "0.4.3": { + "default": { + "path": "Foo.vy/Foo.json", + "build_id": "x", + } + } + } + }, + } + } + } + (tmp_path / "cache" / "solidity-files-cache.json").write_text(json.dumps(cache)) + return tmp_path + + +def test_load_forge_cache(vyper_project): + cache = load_forge_cache(str(vyper_project), str(vyper_project / "out")) + assert cache == {"Foo.vy/Foo.json": ("test/Foo.vy", "0.4.3")} + + +def test_load_forge_cache_missing(tmp_path): + assert load_forge_cache(str(tmp_path), str(tmp_path / "out")) == {} + + +def test_normalize_vyper_artifact(vyper_project): + json_out = forge_vyper_artifact() + contract_type, version, natspec = normalize_vyper_artifact( + json_out, "Foo.vy", str(vyper_project), ("test/Foo.vy", "0.4.3") + ) + + assert contract_type == "contract" + assert version == VYPER_VERSION_PREFIX + "0.4.3" + assert parse_natspec(natspec) == "--loop 4" + + assert set(json_out["methodIdentifiers"]) == { + "check_one(uint256)", + "check_multiline(uint256,uint256[])", + "no_doc(uint256)", + } + assert json_out["methodIdentifiers"]["check_one(uint256)"] == "48ad1bf6" + assert get_source_path(json_out) == "test/Foo.vy" + assert json_out["bytecode"]["linkReferences"] == {} + assert json_out["metadata"]["compiler"]["version"] == "0.4.3" + + assert ( + parse_devdoc("check_one(uint256)", json_out) == "--default-bytes-lengths 0,10" + ) + assert parse_devdoc("check_multiline(uint256,uint256[])", json_out) == "--loop 5" + assert parse_devdoc("no_doc(uint256)", json_out) is None + + +def test_normalize_vyper_selector(): + # keccak256("transfer(address,uint256)")[:4] + json_out = { + "abi": [ + { + "type": "function", + "name": "transfer", + "inputs": [ + {"name": "to", "type": "address"}, + {"name": "amount", "type": "uint256"}, + ], + "outputs": [{"name": "", "type": "bool"}], + "stateMutability": "nonpayable", + }, + {"type": "event", "name": "Transfer", "inputs": [], "anonymous": False}, + ], + "bytecode": {"object": "0x"}, + "deployedBytecode": {"object": "0x"}, + } + normalize_vyper_artifact(json_out, "Token.vy", "/nonexistent", None) + assert json_out["methodIdentifiers"] == {"transfer(address,uint256)": "a9059cbb"} + + +def test_normalize_vyper_interface_and_no_cache(): + json_out = {"abi": [], "bytecode": {"object": "0x"}, "deployedBytecode": {}} + contract_type, version, natspec = normalize_vyper_artifact( + json_out, "IFoo.vyi", "/nonexistent", None + ) + assert contract_type == "interface" + assert version == VYPER_VERSION_PREFIX + "unknown" + assert natspec is None + assert get_source_path(json_out) == "IFoo.vyi" + assert json_out["deployedBytecode"]["object"] == "0x" + + +def test_parse_build_out_vyper(vyper_project): + args = load_config(["--root", str(vyper_project)]) + build_out = parse_build_out(args) + + assert list(build_out) == [VYPER_VERSION_PREFIX + "0.4.3"] + assert has_vyper_contracts(build_out) + + [(view, filename, contract_name)] = list(build_output_iterator(build_out)) + assert (filename, contract_name) == ("Foo.vy", "Foo") + + json_out, contract_type, natspec = view[filename][contract_name] + assert contract_type == "contract" + assert "check_one(uint256)" in json_out["methodIdentifiers"] + assert parse_natspec(natspec) == "--loop 4" + + +def test_build_out_view(): + sol = {"A.sol": {"A": None}} + vy1 = {"B.vy": {"B": None}} + vy2 = {"C.vy": {"C": None}} + build_out = { + "0.8.30": sol, + VYPER_VERSION_PREFIX + "0.4.3": vy1, + VYPER_VERSION_PREFIX + "0.3.10": vy2, + } + + assert set(build_out_view(build_out, "0.8.30")) == {"A.sol", "B.vy", "C.vy"} + assert set(build_out_view(build_out, VYPER_VERSION_PREFIX + "0.4.3")) == { + "A.sol", + "B.vy", + "C.vy", + } + + # solidity is not shared with vyper groups if the solc version is ambiguous + build_out["0.7.6"] = {"D.sol": {"D": None}} + assert set(build_out_view(build_out, VYPER_VERSION_PREFIX + "0.4.3")) == { + "B.vy", + "C.vy", + } + + # no vyper: unchanged, same object + assert build_out_view({"0.8.30": sol}, "0.8.30") is sol + + # every contract is iterated exactly once + iterated = [(f, c) for _, f, c in build_output_iterator(build_out)] + assert sorted(iterated) == [ + ("A.sol", "A"), + ("B.vy", "B"), + ("C.vy", "C"), + ("D.sol", "D"), + ] + + +def test_with_vyper_defaults(): + args = with_vyper_defaults(load_config([])) + assert args.storage_layout == "generic" + assert args.invalid_as_failure + + # explicit settings win + args = with_vyper_defaults(load_config(["--storage-layout", "solidity"])) + assert args.storage_layout == "solidity" + assert args.invalid_as_failure + + # annotations applied later still win over the vyper defaults + args = with_vyper_defaults(load_config([])) + args = args.with_overrides( + ConfigSource.contract_annotation, storage_layout="solidity" + ) + assert args.storage_layout == "solidity" + + +def test_invalid_as_failure_default(): + assert not load_config([]).invalid_as_failure + assert load_config(["--invalid-as-failure"]).invalid_as_failure + + +class FakeOutput: + def __init__(self, error=None, data=b""): + self.error = error + self.data = ByteVec(data) + + +class FakeContext: + def __init__(self, error=None, data=b"", subcalls=()): + self.output = FakeOutput(error, data) + self._subcalls = list(subcalls) + + def last_subcall(self): + return self._subcalls[-1] if self._subcalls else None + + +def test_is_invalid_opcode_found(): + invalid = FakeContext(InvalidOpcode(0xFE)) + empty_revert = FakeContext(Revert()) + ok = FakeContext() + + # directly + assert is_invalid_opcode_found(invalid) + + # bubbled up through empty reverts + assert is_invalid_opcode_found(FakeContext(Revert(), subcalls=[invalid])) + assert is_invalid_opcode_found( + FakeContext(Revert(), subcalls=[FakeContext(Revert(), subcalls=[invalid])]) + ) + + # not an INVALID failure + assert not is_invalid_opcode_found(ok) + assert not is_invalid_opcode_found(empty_revert) + assert not is_invalid_opcode_found(FakeContext(Revert(), subcalls=[ok])) + + # the revert carries its own data, so it's not the bubbled-up INVALID failure + assert not is_invalid_opcode_found( + FakeContext(Revert(), data=b"\x08\xc3\x79\xa0", subcalls=[invalid]) + ) + + # the INVALID failure was caught: the call succeeded + assert not is_invalid_opcode_found(FakeContext(subcalls=[invalid])) diff --git a/tests/vyper/foundry.toml b/tests/vyper/foundry.toml new file mode 100644 index 00000000..a695c5de --- /dev/null +++ b/tests/vyper/foundry.toml @@ -0,0 +1,11 @@ +[profile.default] +src = 'src' +out = 'out' +libs = ['../lib', 'lib'] + +evm_version = 'cancun' + +force = false + +# compile options used by halmos (to prevent unnecessary recompilation when running forge test and halmos together) +extra_output = ["storageLayout", "metadata"] diff --git a/tests/vyper/remappings.txt b/tests/vyper/remappings.txt new file mode 100644 index 00000000..30f1086e --- /dev/null +++ b/tests/vyper/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/vyper/src/Counter.vy b/tests/vyper/src/Counter.vy new file mode 100644 index 00000000..da8f233a --- /dev/null +++ b/tests/vyper/src/Counter.vy @@ -0,0 +1,15 @@ +# pragma version ~=0.4.3 +""" +@title Counter with a bug +""" + +count: public(uint256) + + +@external +def add(x: uint256): + # bug: resets the counter instead of adding 13 + if x == 13: + self.count = 0 + return + self.count += x diff --git a/tests/vyper/src/Immutable.vy b/tests/vyper/src/Immutable.vy new file mode 100644 index 00000000..2a983022 --- /dev/null +++ b/tests/vyper/src/Immutable.vy @@ -0,0 +1,14 @@ +# pragma version ~=0.4.3 + +LIMIT: public(immutable(uint256)) + + +@deploy +def __init__(limit: uint256): + LIMIT = limit + + +@view +@external +def under(x: uint256) -> bool: + return x <= LIMIT diff --git a/tests/vyper/src/Vault.vy b/tests/vyper/src/Vault.vy new file mode 100644 index 00000000..112df63a --- /dev/null +++ b/tests/vyper/src/Vault.vy @@ -0,0 +1,21 @@ +# pragma version ~=0.4.3 + +balanceOf: public(HashMap[address, uint256]) +total: public(uint256) + + +@external +def deposit(amount: uint256): + self.balanceOf[msg.sender] += amount + self.total += amount + + +@external +def withdraw(amount: uint256): + self.balanceOf[msg.sender] -= amount + self.total -= amount + + +@external +def boom(x: uint256): + assert x != 42, UNREACHABLE diff --git a/tests/vyper/test/CounterTest.vy b/tests/vyper/test/CounterTest.vy new file mode 100644 index 00000000..b3752059 --- /dev/null +++ b/tests/vyper/test/CounterTest.vy @@ -0,0 +1,71 @@ +# pragma version ~=0.4.3 +""" +@title Symbolic tests written in Vyper +@custom:halmos --loop 3 +""" + +interface ICounter: + def count() -> uint256: view + def add(x: uint256): nonpayable + + +interface Vm: + def assume(c: bool): nonpayable + def assertEq(a: uint256, b: uint256): nonpayable + def getCode(path: String[64]) -> Bytes[4096]: view + + +VM: constant(address) = 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D + +counter: ICounter + + +@external +def setUp(): + code: Bytes[4096] = staticcall Vm(VM).getCode("Counter.vy:Counter") + self.counter = ICounter(raw_create(code)) + + +@external +def check_add_unreachable(x: uint256): + # expected to fail (x == 13): UNREACHABLE compiles to the INVALID opcode + before: uint256 = staticcall self.counter.count() + extcall self.counter.add(x) + assert staticcall self.counter.count() == before + x, UNREACHABLE + + +@external +def check_add_cheatcode(x: uint256): + # expected to fail (x == 13): hevm assertion cheatcodes work from vyper too + before: uint256 = staticcall self.counter.count() + extcall self.counter.add(x) + extcall Vm(VM).assertEq(staticcall self.counter.count(), before + x) + + +@external +def check_add_ok(x: uint256): + extcall Vm(VM).assume(x != 13) + before: uint256 = staticcall self.counter.count() + extcall self.counter.add(x) + assert staticcall self.counter.count() == before + x, UNREACHABLE + + +@external +def check_plain_assert(x: uint256): + # a plain `assert` is an empty revert, indistinguishable from a `require`: + # the path is discarded, not reported + assert x != 13 + + +@external +def check_bytes_default(data: Bytes[100]): + # expected to fail: 65 is one of the default bytes lengths + assert len(data) != 65, UNREACHABLE + + +@external +def check_bytes_annotated(data: Bytes[100]): + """ + @custom:halmos --default-bytes-lengths 0,10 + """ + assert len(data) != 65, UNREACHABLE diff --git a/tests/vyper/test/VyperTarget.t.sol b/tests/vyper/test/VyperTarget.t.sol new file mode 100644 index 00000000..29c9bd05 --- /dev/null +++ b/tests/vyper/test/VyperTarget.t.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import {Test} from "forge-std/Test.sol"; + +interface ICounter { + function count() external view returns (uint256); + function add(uint256) external; +} + +interface IVault { + function deposit(uint256) external; + function withdraw(uint256) external; + function balanceOf(address) external view returns (uint256); + function total() external view returns (uint256); + function boom(uint256) external; +} + +interface IImmutable { + function LIMIT() external view returns (uint256); + function under(uint256) external view returns (bool); +} + +/// @notice solidity tests for vyper contracts +contract VyperTargetTest is Test { + ICounter counter; + IVault vault; + IImmutable imm; + + function setUp() public { + // all supported getCode path formats + counter = ICounter(deployCode("Counter.vy:Counter")); + vault = IVault(deployCode("src/Vault.vy")); + imm = IImmutable(deployCode("Immutable.vy", abi.encode(uint256(100)))); + } + + function check_counter(uint256 x) public { + // expected to fail (x == 13) + uint256 before = counter.count(); + counter.add(x); + assertEq(counter.count(), before + x); + } + + function check_hashmap(address a, address b, uint256 x, uint256 y) public { + // exercises vyper's HashMap layout, keccak256(slot . key) + vm.assume(a != b); + vm.prank(a); + vault.deposit(x); + vm.prank(b); + vault.deposit(y); + assertEq(vault.balanceOf(a), x); + assertEq(vault.balanceOf(b), y); + assertEq(vault.total(), x + y); + } + + function check_unreachable_in_target(uint256 x) public { + // expected to fail (x == 42): INVALID in the target bubbles up as an empty revert + vault.boom(x); + } + + function check_unreachable_caught(uint256 x) public { + // the failure is caught, so it must not be reported + try vault.boom(x) {} catch {} + } + + function check_immutable(uint256 x) public view { + assertEq(imm.LIMIT(), 100); + assertEq(imm.under(x), x <= 100); + } +} From 9d6c0b6c0d327b2bc324f5af52964f5f3e15a041 Mon Sep 17 00:00:00 2001 From: Hugo DE SA PEREIRA PINTO Date: Tue, 22 Sep 2026 12:02:37 +0200 Subject: [PATCH 18/46] Some renamings to prepare for package publication --- CONTRIBUTING.md | 23 +- NOTICE | 45 + README.md | 179 +- benchmarks/baolean.sh | 2 +- docs/getting-started.md | 34 +- docs/warnings.md | 54 + examples/README.md | 17 +- examples/invariants/README.md | 16 +- examples/invariants/test/ERC20.t.sol | 2 +- examples/invariants/test/ERC721.t.sol | 2 +- examples/invariants/test/MiniVat.t.sol | 2 +- examples/invariants/test/Reentrancy.t.sol | 2 +- examples/invariants/test/SimpleState.t.sol | 2 +- examples/invariants/test/Vat.t.sol | 2 +- examples/simple/README.md | 12 +- examples/simple/foundry.toml | 2 +- examples/simple/test/BadElections.t.sol | 4 +- examples/simple/test/IsPowerOfTwo.t.sol | 6 +- examples/simple/test/TotalPrice.t.sol | 2 +- examples/simple/test/Vault.t.sol | 4 +- .../tokens/ERC20/test/BackdoorERC20.t.sol | 4 +- examples/tokens/ERC20/test/CurveTokenV3.t.sol | 4 +- .../tokens/ERC20/test/DEIStablecoin.t.sol | 4 +- .../tokens/ERC20/test/OpenZeppelinERC20.t.sol | 4 +- examples/tokens/ERC20/test/SoladyERC20.t.sol | 4 +- examples/tokens/ERC20/test/SolmateERC20.t.sol | 4 +- .../ERC721/test/OpenZeppelinERC721.t.sol | 4 +- .../tokens/ERC721/test/SoladyERC721.t.sol | 4 +- .../tokens/ERC721/test/SolmateERC721.t.sol | 4 +- .../Dockerfile | 4 +- packages/{halmos => dolmos}/Dockerfile | 20 +- packages/dolmos/README.md | 15 + packages/halmos/README.md | 15 - packages/solvers/README.md | 8 +- pyproject.toml | 13 +- src/{halmos => dolmos}/__init__.py | 0 src/{halmos => dolmos}/__main__.py | 102 +- src/{halmos => dolmos}/assertions.py | 6 +- src/{halmos => dolmos}/bitvec.py | 338 +- src/{halmos => dolmos}/build.py | 60 +- src/{halmos => dolmos}/bytevec.py | 6 +- src/{halmos => dolmos}/calldata.py | 14 +- src/{halmos => dolmos}/cheatcodes.py | 78 +- src/{halmos => dolmos}/config.py | 43 +- src/{halmos => dolmos}/console.py | 6 +- src/{halmos => dolmos}/constants.py | 2 +- src/{halmos => dolmos}/contract.py | 14 +- src/{halmos => dolmos}/env.py | 4 +- src/{halmos => dolmos}/exceptions.py | 4 +- src/{halmos => dolmos}/flamegraphs.py | 8 +- src/{halmos => dolmos}/hashes.py | 0 src/{halmos => dolmos}/logs.py | 6 +- src/{halmos => dolmos}/mapper.py | 14 +- src/{halmos => dolmos}/memtrace.py | 2 +- src/{halmos => dolmos}/processes.py | 4 +- src/{halmos => dolmos}/sevm.py | 84 +- src/{halmos => dolmos}/solve.py | 32 +- src/{halmos => dolmos}/solvers.py | 23 +- src/{halmos => dolmos}/traces.py | 16 +- src/{halmos => dolmos}/ui.py | 0 src/{halmos => dolmos}/utils.py | 16 +- tests/conftest.py | 4 +- tests/expected/all.json | 2959 +++++++++-------- tests/ffi/foundry.toml | 2 +- tests/regression/{halmos.toml => dolmos.toml} | 8 +- tests/regression/foundry.toml | 2 +- tests/regression/src/Create.sol | 4 +- tests/regression/test/Arith.t.sol | 2 +- tests/regression/test/AssertTest.t.sol | 2 +- tests/regression/test/Context.t.sol | 8 +- tests/regression/test/Counter.t.sol | 2 +- tests/regression/test/Create.t.sol | 2 +- tests/regression/test/Create3.t.sol | 2 +- tests/regression/test/Foundry.t.sol | 2 +- tests/regression/test/Getter.t.sol | 4 +- tests/regression/test/Invalid.t.sol | 4 +- tests/regression/test/InvariantProbes.t.sol | 2 +- tests/regression/test/InvariantTarget.t.sol | 2 +- .../regression/test/InvariantTargetThis.t.sol | 4 +- tests/regression/test/MegaMem.t.sol | 2 +- tests/regression/test/Natspec.t.sol | 36 +- tests/regression/test/Panic.t.sol | 32 +- tests/regression/test/Revert.t.sol | 6 +- tests/regression/test/Send.t.sol | 4 +- tests/regression/test/SetupPlus.t.sol | 2 +- tests/regression/test/Sha3.t.sol | 2 +- tests/regression/test/Signature.t.sol | 2 +- tests/regression/test/SmolWETH.t.sol | 2 +- tests/regression/test/Snapshot.t.sol | 4 +- tests/regression/test/Solver.t.sol | 4 +- tests/regression/test/StdAssertTest.t.sol | 8 +- tests/regression/test/Storage5.t.sol | 2 +- tests/regression/test/StorageSlot.t.sol | 6 +- tests/regression/test/Store.t.sol | 2 +- tests/regression/test/Struct.t.sol | 10 +- ...lmosCheatCode.t.sol => SvmCheatCode.t.sol} | 32 +- tests/regression/test/TStore.t.sol | 2 +- tests/regression/test/Token.t.sol | 2 +- .../regression/test/UniswapPairAddress.t.sol | 2 +- tests/solver/foundry.toml | 2 +- tests/solver/test/Math.t.sol | 4 +- tests/solver/test/Solver.t.sol | 4 +- tests/test_bitvec.py | 10 +- tests/test_bytevec.py | 4 +- tests/test_cli.py | 8 +- tests/test_config.py | 4 +- tests/{test_halmos.py => test_dolmos.py} | 20 +- tests/test_fixtures.py | 12 +- tests/test_mapper.py | 2 +- tests/test_natspec.py | 36 + tests/test_prank.py | 12 +- tests/test_sevm.py | 12 +- tests/test_solve.py | 10 +- tests/test_source_map.py | 2 +- tests/test_traces.py | 16 +- tests/test_utils.py | 4 +- tests/test_vyper.py | 14 +- tests/vyper/foundry.toml | 2 +- tests/vyper/test/CounterTest.vy | 4 +- 119 files changed, 2476 insertions(+), 2288 deletions(-) create mode 100644 NOTICE create mode 100644 docs/warnings.md rename packages/{halmos-builder => dolmos-builder}/Dockerfile (92%) rename packages/{halmos => dolmos}/Dockerfile (70%) create mode 100644 packages/dolmos/README.md delete mode 100644 packages/halmos/README.md rename src/{halmos => dolmos}/__init__.py (100%) rename src/{halmos => dolmos}/__main__.py (96%) rename src/{halmos => dolmos}/assertions.py (98%) rename src/{halmos => dolmos}/bitvec.py (71%) rename src/{halmos => dolmos}/build.py (91%) rename src/{halmos => dolmos}/bytevec.py (99%) rename src/{halmos => dolmos}/calldata.py (97%) rename src/{halmos => dolmos}/cheatcodes.py (96%) rename src/{halmos => dolmos}/config.py (96%) rename src/{halmos => dolmos}/console.py (97%) rename src/{halmos => dolmos}/constants.py (83%) rename src/{halmos => dolmos}/contract.py (98%) rename src/{halmos => dolmos}/env.py (98%) rename src/{halmos => dolmos}/exceptions.py (97%) rename src/{halmos => dolmos}/flamegraphs.py (97%) rename src/{halmos => dolmos}/hashes.py (100%) rename src/{halmos => dolmos}/logs.py (91%) rename src/{halmos => dolmos}/mapper.py (98%) rename src/{halmos => dolmos}/memtrace.py (99%) rename src/{halmos => dolmos}/processes.py (98%) rename src/{halmos => dolmos}/sevm.py (98%) rename src/{halmos => dolmos}/solve.py (96%) rename src/{halmos => dolmos}/solvers.py (96%) rename src/{halmos => dolmos}/traces.py (95%) rename src/{halmos => dolmos}/ui.py (100%) rename src/{halmos => dolmos}/utils.py (99%) rename tests/regression/{halmos.toml => dolmos.toml} (92%) rename tests/regression/test/{HalmosCheatCode.t.sol => SvmCheatCode.t.sol} (96%) rename tests/{test_halmos.py => test_dolmos.py} (86%) create mode 100644 tests/test_natspec.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4c385ca9..b8bf7e56 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,10 +1,7 @@ -# Contributing to Halmos +# Contributing to dolmos -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]: +Bug reports, suggestions and pull requests are welcome on +[GitHub](https://github.com/Dowsers/dolmos/issues). ## Development Setup @@ -12,13 +9,13 @@ Clone or fork the repository: ```sh # if you want to submit a pull request, fork the repository: -gh repo fork a16z/halmos +gh repo fork Dowsers/dolmos # Or, if you just want to develop locally, clone it: -git clone git@github.com:a16z/halmos.git +git clone git@github.com:Dowsers/dolmos.git # navigate to the project directory -cd halmos +cd dolmos ``` **Recommended**: set up the development environment using [uv](https://docs.astral.sh/uv/): @@ -39,8 +36,8 @@ uv sync --extra dev uv run pre-commit install uv run pre-commit run --all-files -# make changes to halmos, then run it with: -uv run halmos +# make changes to dolmos, then run it with: +uv run dolmos # run the tests with: uv run pytest @@ -65,7 +62,7 @@ Alternatively, you can manage the python version and the virtual environment man # create and activate a virtual environment with a suitable python version python3.12 -m venv .venv && source .venv/bin/activate -# install halmos and its runtime dependencies in editable mode +# install dolmos and its runtime dependencies in editable mode python -m pip install -e ".[dev]" # install and run the pre-commit hooks @@ -94,4 +91,4 @@ By contributing, you agree that your contributions will be licensed under its [A ## 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._ +See the disclaimer in the [README](README.md#disclaimer). diff --git a/NOTICE b/NOTICE new file mode 100644 index 00000000..e0bfcf3b --- /dev/null +++ b/NOTICE @@ -0,0 +1,45 @@ +dolmos +Copyright (C) 2026 Dowsers SAS + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU Affero General Public License as published by the Free +Software Foundation, version 3. See LICENSE. + + +ORIGIN +====== + +dolmos is a modified version of halmos (https://github.com/a16z/halmos), +a symbolic testing tool for EVM smart contracts developed by a16z crypto and its +contributors (maintainers at the time of the fork: Daejun Park, karmacoma), +distributed under the GNU Affero General Public License v3. + +dolmos is based on halmos as of upstream commit "fix: shorten bytecode output in +unknown deployed bytecode warning (#573)" (2025-08-06). Dowsers SAS has modified +it since 2026-05-15; the modifications include CREATE address derivation and +account nonces, nonce cheatcodes, the CLZ, BLOBHASH and BLOBBASEFEE opcodes, +Vyper support, and the renaming of the command, the Python package and the +Docker images to dolmos. The git history records every change. + +dolmos is maintained by Dowsers SAS and is not affiliated with or endorsed by +a16z crypto or the halmos maintainers. + + +COMPATIBILITY WITH HALMOS +========================= + +For compatibility with existing test suites, dolmos still accepts the +`@custom:halmos` natspec tag, the `halmos.toml` configuration file name and the +HALMOS_ALLOW_DOWNLOAD environment variable, and it supports the cheatcodes of +the halmos-cheatcodes Solidity library (https://github.com/a16z/halmos-cheatcodes), +which is a separate project used by the test suite as a git submodule. + + +DISCLAIMER OF THE ORIGINAL WORK +=============================== + +The following disclaimer accompanies halmos and applies to the original work: + +_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._ + +The same absence of warranty applies to the modifications made by Dowsers SAS. diff --git a/README.md b/README.md index 8b72ebe5..36087a3b 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,28 @@ # Dolmos -[![License](https://img.shields.io/github/license/HugoDowsers/dolmos)](LICENSE) -[![Python Version from PEP 621 TOML](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2FHugoDowsers%2Fdolmos%2Frefs%2Fheads%2Fmain%2Fpyproject.toml)](pyproject.toml) -[![upstream](https://img.shields.io/badge/fork%20of-a16z%2Fhalmos-blue)](https://github.com/a16z/halmos) - -Dolmos is the [Dowsers](https://github.com/Dowsers) fork of -[a16z/halmos](https://github.com/a16z/halmos), a _symbolic testing_ tool for EVM -smart contracts driven by Foundry tests. - -The fork exists to carry fixes and EVM coverage we need for audit work, ahead of -(or instead of) upstream. Everything upstream does, dolmos does; this README only -documents what differs. For the tool itself, read the upstream -[getting started guide](docs/getting-started.md) and the -[examples](examples/README.md) — they apply unchanged, modulo the command name. - -The command is `dolmos`. The Python package is still named `halmos`, so -`import halmos` and rebases on upstream keep working. - -## What this fork adds - -**CREATE address derivation and account nonces.** Upstream assigns CREATE -addresses from a counter and does not model nonces at all, so a contract that -predicts its own deployment address disagrees with the executor. Every CREATE3 -library breaks that way ([a16z/halmos#217](https://github.com/a16z/halmos/issues/217)): -all paths revert and the test reports `paths: 0`. Dolmos tracks nonces (1 for a +[![License](https://img.shields.io/github/license/Dowsers/dolmos)](LICENSE) +[![Python Version from PEP 621 TOML](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2FDowsers%2Fdolmos%2Frefs%2Fheads%2Fmain%2Fpyproject.toml)](pyproject.toml) + +Dolmos is a _symbolic testing_ tool for EVM smart contracts, driven by +Foundry-style tests, maintained by [Dowsers](https://github.com/Dowsers) for its +audit and formal verification work. Tests look like fuzz tests, but inputs are +symbolic: a passing test holds for every input, and a failing one comes with a +counterexample. + +Start with the [getting started guide](docs/getting-started.md) and the +[examples](examples/README.md). + +## Features + +On top of symbolic execution of Solidity (and Vyper) tests with `check_*` and +`invariant_*` functions, dolmos provides the following. + +**CREATE address derivation and account nonces.** Accounts carry nonces (1 for a newly created account per EIP-161, incremented by CREATE and CREATE2 even when -the creation fails) and derives the address from `keccak256(rlp([sender, nonce]))`, -mapped to a magic address exactly as CREATE2 already was. Both sides of the -prediction then agree. +the creation fails), and CREATE addresses are derived from +`keccak256(rlp([sender, nonce]))`, mapped to a magic address as for CREATE2. A +contract that predicts its own deployment address, as every CREATE3 library +does, therefore agrees with the executor. **Nonce cheatcodes.** `vm.getNonce`, `vm.setNonce`, `vm.setNonceUnsafe` and `vm.resetNonce`, with foundry's semantics: `setNonce` only raises a nonce, @@ -37,23 +32,21 @@ prediction then agree. logarithmic symbolic encoding, plus `BLOBHASH` and `BLOBBASEFEE` (EIP-4844). **Vyper.** Test contracts can be written in Vyper, and Solidity tests can target -Vyper contracts. Upstream only reads the `.sol` directories of the build output, -and forge's Vyper artifacts have no AST, no metadata and an empty -`methodIdentifiers`; dolmos reads `.vy`/`.vyi` artifacts too, computes the -selectors from the ABI, takes the source path and compiler version from forge's -cache, and makes Vyper contracts visible to Solidity tests (deployed code, ABI -for invariant targets and traces, including contracts with immutables). +Vyper contracts. forge's Vyper artifacts have no AST, no metadata and an empty +`methodIdentifiers`; dolmos reads `.vy`/`.vyi` artifacts, computes the selectors +from the ABI, takes the source path and compiler version from forge's cache, and +makes Vyper contracts visible to Solidity tests (deployed code, ABI for +invariant targets and traces, including contracts with immutables). `vm.getCode` accepts `Foo.vy`, `src/Foo.vy` and `Foo.vy:Foo`. A Vyper test looks like a Solidity one: `setUp()` and `check_*`/`invariant_*` functions, with cheatcodes called through an interface at `0x7109709ECfa91a80626fF3989D68f67F5b1DD12D`. Use `assert ..., UNREACHABLE` for -properties: it compiles to the INVALID opcode, which the new -`--invalid-as-failure` option reports as a failure, even when it is hit in a -nested call and bubbles up as an empty revert. A plain `assert` is an empty -revert, indistinguishable from a `require`, so the path is silently discarded. -The `vm.assert*` cheatcodes work as well. `@custom:halmos` annotations go in the -module and function docstrings. +properties: it compiles to the INVALID opcode, which the `--invalid-as-failure` +option reports as a failure, even when it is hit in a nested call and bubbles up +as an empty revert. A plain `assert` is an empty revert, indistinguishable from +a `require`, so the path is silently discarded. The `vm.assert*` cheatcodes work +as well. `@custom:dolmos` annotations go in the module and function docstrings. When the build contains Vyper contracts, `--storage-layout generic` (Vyper computes `HashMap` slots as `keccak256(slot . key)`) and `--invalid-as-failure` @@ -61,33 +54,30 @@ become the defaults; a value set in `dolmos.toml`, an annotation or on the command line still wins. Coverage reports do not include Vyper code, since forge emits no source maps for it. See `tests/vyper` for examples. -**CLI.** The command is `dolmos`; `halmos` is no longer installed. A project -config file may be named `dolmos.toml` (read before `halmos.toml`), `--help` and -`--version` report the name the command was invoked with, and `-test` is a short -alias for `--match-test` (`dolmos -test setNonce`). - -## Not available in this fork +## Compatibility with halmos -These exist upstream and are **not** provided here yet: +Dolmos is derived from halmos (see [NOTICE](NOTICE)) and accepts the names used +by test suites written for it, so they should run without changes: -| | status | -|---|---| -| PyPI package (`pip install dolmos`) | not published — install from source | -| `uv tool install dolmos` | not available — depends on the PyPI package | -| Docker image (`ghcr.io/...`) | not built — no published image | -| Prebuilt release binaries | none | -| CI workflows | inherited from upstream and currently broken on this fork, since they invoke the `halmos` command and the a16z docker image | +| halmos | dolmos | | +|---|---|---| +| `@custom:halmos` | `@custom:dolmos` | both accepted, in contracts and functions | +| `halmos.toml` | `dolmos.toml` | `dolmos.toml` is read first | +| `HALMOS_ALLOW_DOWNLOAD` | `DOLMOS_ALLOW_DOWNLOAD` | both honored | +| `halmos-cheatcodes` | unchanged | the `svm.*` cheatcodes of this library are supported | -Until then, install from source as described below. If you want the packaged -experience today, use upstream halmos instead — the two can coexist in the same -environment, they no longer share a command name. +Some things do change: the command and the Python package are named `dolmos` +(`import dolmos`), symbolic variables in counterexamples are prefixed with +`dolmos_`, and solvers downloaded on demand are cached in `~/.dolmos/solvers`. ## Installation -Requires Python ≥ 3.11 and [Foundry](https://getfoundry.sh). +Requires Python ≥ 3.11 and [Foundry](https://getfoundry.sh). Dolmos is not +published on PyPI and has no prebuilt binaries or published Docker image yet, so +install it from source: ```sh -git clone https://github.com/HugoDowsers/dolmos.git +git clone https://github.com/Dowsers/dolmos.git cd dolmos python3.12 -m venv .venv && source .venv/bin/activate pip install -e . @@ -101,10 +91,11 @@ pip install -e ".[dev]" pre-commit install ``` -If a `halmos` command lingers in your `PATH` after installing, it comes from a -separate installation (typically a `pip install --user` one). Check with -`type -a halmos`, and `python -c "import halmos, os; print(os.path.dirname(halmos.__file__))"` -to confirm which source tree is actually being imported. +The Docker images are built by the `publish-solvers-package`, +`publish-dolmos-builder-package` and `publish-dolmos-package` workflows, in that +order, and pushed to `ghcr.io//`. The workflows that run tests inside +those images (`test-ffi`, `test-long`, `test-external`) need them to be +published first. ## Usage @@ -120,16 +111,23 @@ dolmos -test # alias for --match-test dolmos --match-contract ``` -Options can be set per project in `dolmos.toml` at the project root, with the -same format as upstream's `halmos.toml`: +Options can be set per project in `dolmos.toml` at the project root: ```toml [global] solver-timeout-assertion = 10000 ``` +`python -m dolmos.config` prints every option with its default value, in that +format. Warnings link to their explanation in [docs/warnings.md](docs/warnings.md). + ## Tests +```sh +pytest # unit tests and integration tests +pytest tests/test_dolmos.py -k "not long" +``` + The regression suite lives in `tests/regression` and runs with both forge (real EVM) and dolmos (symbolic): @@ -139,51 +137,44 @@ forge build dolmos ``` -Two notes on the toolchain. The suite sets `evm_version = 'osaka'` because of -the CLZ tests, and `test/OpCodesCLZ.sol` needs a solc recent enough to accept -`clz()` in assembly — an older compiler fails with `Function "clz" not found`. -And `test/Invalid.t.sol`, inherited from upstream, requires solc `^0.5.2`; if -that version is unavailable, `forge build` stops there. +A few notes on the toolchain: -The Vyper tests live in `tests/vyper` and need the `vyper` compiler in `PATH` -(`pip install vyper`); pytest skips them otherwise. +- The suite sets `evm_version = 'osaka'` because of the CLZ tests, and + `test/OpCodesCLZ.sol` needs a solc recent enough to accept `clz()` in assembly + (0.8.31 or later); an older compiler fails with `Function "clz" not found`. +- `test/Invalid.t.sol` requires solc `^0.5.2`; if that version is unavailable, + `forge build` stops there. +- `test/Arith.t.sol` uses the `bitwuzla-abs` solver; set + `DOLMOS_ALLOW_DOWNLOAD=1` to let dolmos download it. +- The Vyper tests live in `tests/vyper` and need the `vyper` compiler in `PATH` + (`pip install vyper`); pytest skips them otherwise. ## Contributing -Fixes that are not specific to Dowsers should go upstream: branch off -`upstream/main`, keep the branch free of any fork-specific change, and open the -pull request against -[a16z/halmos](https://github.com/a16z/halmos). The CREATE3 work above is written -that way and is meant to be proposed. +See [CONTRIBUTING.md](CONTRIBUTING.md). ## Roadmap -Work in progress, not merged here yet: - - MODEXP precompile (EIP-198): concrete evaluation and a typed uninterpreted function instead of the current opaque one - performance: skip the Z3 `substitute()` traversal when no free variable matches, bound the concurrency of solver subprocesses, memoize invariant target resolution -- support multiple `setUp()` states ([a16z/halmos#186](https://github.com/a16z/halmos/issues/186)) -- docker image and a published package +- support multiple `setUp()` states +- a published package and Docker image ## License -AGPL-3.0, inherited from upstream. See [LICENSE](LICENSE). +AGPL-3.0, see [LICENSE](LICENSE). Dolmos is a modified version of another +AGPL-3.0 program; [NOTICE](NOTICE) gives its origin and the notices that come +with it. ## 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._ - -This fork is maintained by Dowsers and is not affiliated with or endorsed by -a16z. The upstream disclaimer above applies to the original work; the same lack -of warranty applies to the changes made here. +_This software and the smart contracts in this repository are provided as is, +without warranty of any kind, express or implied, including any warranty of +merchantability, non-infringement or fitness for a particular purpose. They have +not been audited. Passing symbolic tests is not a proof that a contract is free +of bugs. Nothing in this repository is investment or legal advice. See +[NOTICE](NOTICE) for the disclaimers attached to the original work this code is +derived from._ diff --git a/benchmarks/baolean.sh b/benchmarks/baolean.sh index 73f2229d..2c37b262 100755 --- a/benchmarks/baolean.sh +++ b/benchmarks/baolean.sh @@ -13,6 +13,6 @@ 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 + time dolmos --root symexec-bench/SymTest --contract ${test_name} "--function" test done diff --git a/docs/getting-started.md b/docs/getting-started.md index bfac18cc..497c5511 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,4 +1,4 @@ -# How to write symbolic tests with Halmos +# How to write symbolic tests with dolmos Symbolic tests look 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. @@ -6,21 +6,17 @@ Symbolic tests look similar to fuzz tests, but there are certain differences tha [Foundry]: [Foundry Fuzz Testing]: -## 0. Install Halmos +## 0. Install dolmos -If you haven't installed Halmos yet, please refer to the [installation guide](../README.md#installation) or quickly install it with: - -```sh -uv tool install halmos -``` +If you haven't installed dolmos yet, follow the [installation guide](../README.md#installation). ## 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]. +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 the cheatcodes of the [halmos-cheatcodes] library, which dolmos supports. -[Halmos cheatcodes]: +[halmos-cheatcodes]: For example, consider a basic ERC20 token contract as shown below: ```solidity @@ -53,12 +49,12 @@ By using the symbolic initial supply, you can check if the given tests pass for **Tips:** -- The Halmos cheatcodes can be installed like any other Solidity dependencies: +- The cheatcodes library can be installed like any other Solidity dependency: ``` forge install a16z/halmos-cheatcodes ``` -- The current list of available Halmos cheatcodes can be found [here][halmos-cheatcodes-list]. +- The current list of available cheatcodes can be found [here][halmos-cheatcodes-list]. [halmos-cheatcodes-list]: @@ -128,7 +124,7 @@ for (uint160 sender = 0; sender < type(uint160).max; sender++) { **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: +- Instead of declaring symbolic input parameters, you can dynamically create symbols inside the test using the symbolic cheatcodes. For instance, our running example can be rewritten as follows: ```solidity function check_transfer() { address sender = svm.createAddress("sender"); @@ -138,7 +134,7 @@ for (uint160 sender = 0; sender < type(uint160).max; sender++) { } ``` -- 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: +- Dolmos 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'); ``` @@ -210,15 +206,15 @@ 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. +If there are any inputs that violate these assertions, dolmos will report 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. +For our example, dolmos 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. +- Dolmos focuses solely on assertion violations (i.e., revert with `Panic(1)`), disregarding other revert cases. This means that dolmos 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: +- 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 dolmos will _not_ report any counterexamples by default. In that case, run with `--invalid-as-failure` (the default when the build contains Vyper contracts), or use a custom assertion that reverts with `Panic(1)` upon failure, as shown below: ```solidity function myAssert(bool cond) internal pure { if (!cond) { @@ -245,7 +241,3 @@ However, since symbolic tests are performed symbolically, certain behavioral dif - 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/docs/warnings.md b/docs/warnings.md new file mode 100644 index 00000000..c56f7465 --- /dev/null +++ b/docs/warnings.md @@ -0,0 +1,54 @@ +# Warnings + +Each warning printed by dolmos ends with a link to its section below. + +## parsing-error + +A build artifact in the forge output directory could not be parsed and was +skipped, so the contracts it defines are neither tested nor available to +`vm.getCode`/`deployCode`. Rebuild with `forge build --force`; if the artifact +comes from an unusual compiler setup, run with `--debug` to see the full error. + +## internal-error + +Symbolic execution hit a case dolmos does not handle, or a cheatcode was used +in a way it does not support (for instance `vm.ffi` without `--ffi`). The +affected path is dropped, so the result of the test may be incomplete. Run with +`--debug` for details, and report it if the input looks legitimate. + +## library-placeholder + +The bytecode still contains unlinked library placeholders (`__$...$__`). Only +libraries compiled in the same build can be linked; deploy or link external +libraries explicitly in `setUp()`. + +## counterexample-invalid + +The solver produced a model for a failing path, but replaying it did not +reproduce the failure. This usually comes from approximations in the symbolic +model (hash functions, unbounded arrays, precompiles). The test is still +reported as failing; inspect the counterexample before trusting it. + +## counterexample-unknown + +The solver could not decide whether a failing path is feasible within +`--solver-timeout-assertion`. The test is reported as a timeout rather than a +pass. Increase the timeout, try another `--solver`, or reduce the input space +with `vm.assume` and smaller `--array-lengths`. + +## unsupported-opcode + +An opcode dolmos does not implement was executed; the path is dropped. + +## revert-all + +Every path of the test reverted, so nothing was actually checked. Typical +causes: `vm.assume` conditions that can never hold, a `setUp()` state that makes +the target always revert, or input bounds that are too tight. + +## loop-bound + +Some paths were cut at the loop unrolling bound (`--loop`), so they have not +been fully explored and bugs beyond that bound can be missed. Raise `--loop` +(globally or with a `@custom:dolmos --loop N` annotation) if the loops in +question are bounded by a small constant. diff --git a/examples/README.md b/examples/README.md index 88569593..7187f717 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,4 +1,4 @@ -# Halmos Examples +# Dolmos Examples #### Usage Examples @@ -8,7 +8,12 @@ - Includes identifying the DEI token bug exploited in the [Deus DAO hack](https://rekt.news/deus-dao-r3kt/). - [ERC721](tokens/ERC721/test/): verifies OpenZeppelin, Solady, and Solmate ERC721 tokens. -#### Halmos Tests in External Projects +#### Symbolic Tests in External Projects + +These suites were written for halmos; thanks to dolmos' compatibility with +`@custom:halmos` annotations and `halmos.toml` they should run with dolmos as +they are. + - [Morpho Blue] ([HalmosTest]): verifies the Morpho Blue protocol. - [Farcaster] ([IdRegistrySymTest], [KeyRegistrySymTest]): verifies state machine invariants in Farcaster's onchain registry contracts. @@ -36,4 +41,10 @@ ## 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._ +_This software and the smart contracts in this repository are provided as is, +without warranty of any kind, express or implied, including any warranty of +merchantability, non-infringement or fitness for a particular purpose. They have +not been audited. Passing symbolic tests is not a proof that a contract is free +of bugs. Nothing in this repository is investment or legal advice. See +[NOTICE](../NOTICE) for the disclaimers attached to the original work this code is +derived from._ diff --git a/examples/invariants/README.md b/examples/invariants/README.md index 21f0a1a7..823bc9ca 100644 --- a/examples/invariants/README.md +++ b/examples/invariants/README.md @@ -1,6 +1,6 @@ -# Halmos Invariant Testing Examples +# Dolmos Invariant Testing Examples -This directory contains examples demonstrating various approaches to invariant testing with halmos. +This directory contains examples demonstrating various approaches to invariant testing with dolmos. ## Examples @@ -8,13 +8,13 @@ This directory contains examples demonstrating various approaches to invariant t - **ERC721 Test** ([`ERC721.t.sol`](test/ERC721.t.sol)): Simple ERC721 invariant testing without handlers. Demonstrates how straightforward invariant testing can be when state tracking isn't needed. -- **MiniVat Test** ([`MiniVat.t.sol`](test/MiniVat.t.sol)): Halmos version of [Antonio's MiniVat test](https://github.com/aviggiano/property-based-testing-benchmark/blob/main/projects/dai-certora/test/TargetFunctions.sol). Unlike other fuzzers, no bounds are needed, so no handlers are required. Simply deploy the contract and write the invariant. +- **MiniVat Test** ([`MiniVat.t.sol`](test/MiniVat.t.sol)): dolmos version of [Antonio's MiniVat test](https://github.com/aviggiano/property-based-testing-benchmark/blob/main/projects/dai-certora/test/TargetFunctions.sol). Unlike other fuzzers, no bounds are needed, so no handlers are required. Simply deploy the contract and write the invariant. - **Vat Test** ([`Vat.t.sol`](test/Vat.t.sol)): Tests the full Vat contract using the same invariant as MiniVat. It is more comprehensive and takes longer to run. -- **SimpleState Test** ([`SimpleState.t.sol`](test/SimpleState.t.sol)): Example from the [ItyFuzz paper](https://arxiv.org/pdf/2306.17135) that's hard to detect with simple random testing. Halmos uses exhaustive search and considers only unique state-generating call sequences for efficient space exploration. +- **SimpleState Test** ([`SimpleState.t.sol`](test/SimpleState.t.sol)): Example from the [ItyFuzz paper](https://arxiv.org/pdf/2306.17135) that's hard to detect with simple random testing. Dolmos uses exhaustive search and considers only unique state-generating call sequences for efficient space exploration. -- **Reentrancy Test** ([`Reentrancy.t.sol`](test/Reentrancy.t.sol)): Shows how to detect reentrancy exploits using halmos invariant testing. Demonstrates attacker contract implementation using [halmos cheatcodes](https://github.com/a16z/halmos-cheatcodes). +- **Reentrancy Test** ([`Reentrancy.t.sol`](test/Reentrancy.t.sol)): Shows how to detect reentrancy exploits using dolmos invariant testing. Demonstrates attacker contract implementation using the [halmos-cheatcodes](https://github.com/a16z/halmos-cheatcodes) library. ## Running Tests @@ -22,11 +22,11 @@ This directory contains examples demonstrating various approaches to invariant t cd examples/invariants # Run all invariant tests -halmos +dolmos # Run specific test -halmos --contract ERC20Test +dolmos --contract ERC20Test # Run specific invariant -halmos --function invariant_sumOfBalancesEqualsTotalSupply +dolmos --function invariant_sumOfBalancesEqualsTotalSupply ``` diff --git a/examples/invariants/test/ERC20.t.sol b/examples/invariants/test/ERC20.t.sol index 577c320b..1ed5e43f 100644 --- a/examples/invariants/test/ERC20.t.sol +++ b/examples/invariants/test/ERC20.t.sol @@ -6,7 +6,7 @@ pragma solidity >=0.8.0 <0.9.0; import {ERC20} from "../src/ERC20.sol"; import {Test} from "forge-std/Test.sol"; -/// @custom:halmos --solver bitwuzla-abs --loop 3 --solver-timeout-assertion 0 +/// @custom:dolmos --solver bitwuzla-abs --loop 3 --solver-timeout-assertion 0 contract ERC20Test is Test { // token address ERC20 internal token; diff --git a/examples/invariants/test/ERC721.t.sol b/examples/invariants/test/ERC721.t.sol index e892be54..55835a4b 100644 --- a/examples/invariants/test/ERC721.t.sol +++ b/examples/invariants/test/ERC721.t.sol @@ -4,7 +4,7 @@ pragma solidity >=0.8.0 <0.9.0; import {ERC721} from "../src/ERC721.sol"; import {Test} from "forge-std/Test.sol"; -/// @custom:halmos --solver bitwuzla-abs --solver-timeout-assertion 0 +/// @custom:dolmos --solver bitwuzla-abs --solver-timeout-assertion 0 contract ERC721Test is Test { // ERC721 token address ERC721 internal token; diff --git a/examples/invariants/test/MiniVat.t.sol b/examples/invariants/test/MiniVat.t.sol index 86aca923..431d9264 100644 --- a/examples/invariants/test/MiniVat.t.sol +++ b/examples/invariants/test/MiniVat.t.sol @@ -7,7 +7,7 @@ import "forge-std/Test.sol"; import {MiniVat} from "../src/MiniVat.sol"; -/// @custom:halmos --early-exit +/// @custom:dolmos --early-exit contract MiniVatTest is Test { MiniVat public minivat; diff --git a/examples/invariants/test/Reentrancy.t.sol b/examples/invariants/test/Reentrancy.t.sol index 171bc281..adf819f0 100644 --- a/examples/invariants/test/Reentrancy.t.sol +++ b/examples/invariants/test/Reentrancy.t.sol @@ -68,7 +68,7 @@ contract Attacker is SymTest, Test { } } -/// @custom:halmos --early-exit +/// @custom:dolmos --early-exit contract ReentrancyTest is Test, ERC1155Holder { ERC1155Mock token; BuggyVault vault; diff --git a/examples/invariants/test/SimpleState.t.sol b/examples/invariants/test/SimpleState.t.sol index bb96d843..47eabb20 100644 --- a/examples/invariants/test/SimpleState.t.sol +++ b/examples/invariants/test/SimpleState.t.sol @@ -30,7 +30,7 @@ contract SimpleStateTest is Test { target = new SimpleState(); } - /// @custom:halmos --invariant-depth 10 + /// @custom:dolmos --invariant-depth 10 function invariant_buggy() public view { assertFalse(target.buggy()); } diff --git a/examples/invariants/test/Vat.t.sol b/examples/invariants/test/Vat.t.sol index a9ae8aaf..530dd81b 100644 --- a/examples/invariants/test/Vat.t.sol +++ b/examples/invariants/test/Vat.t.sol @@ -5,7 +5,7 @@ import "forge-std/Test.sol"; import {Vat} from "../src/Vat.sol"; -/// @custom:halmos --early-exit +/// @custom:dolmos --early-exit contract VatTest is Test { Vat public vat; bytes32 ilk; diff --git a/examples/simple/README.md b/examples/simple/README.md index 0ed140b1..10ce25b7 100644 --- a/examples/simple/README.md +++ b/examples/simple/README.md @@ -29,13 +29,19 @@ $ forge test 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 +$ dolmos --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!)_ +_(In this specific example, Dolmos 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._ +_This software and the smart contracts in this repository are provided as is, +without warranty of any kind, express or implied, including any warranty of +merchantability, non-infringement or fitness for a particular purpose. They have +not been audited. Passing symbolic tests is not a proof that a contract is free +of bugs. Nothing in this repository is investment or legal advice. See +[NOTICE](../../NOTICE) for the disclaimers attached to the original work this code is +derived from._ diff --git a/examples/simple/foundry.toml b/examples/simple/foundry.toml index f9e9259f..40162b39 100644 --- a/examples/simple/foundry.toml +++ b/examples/simple/foundry.toml @@ -6,5 +6,5 @@ libs = ["../../tests/lib", 'lib'] evm_version = 'cancun' force = false -# compile options used by halmos (to prevent unnecessary recompilation when running forge test and halmos together) +# compile options used by dolmos (to prevent unnecessary recompilation when running forge test and dolmos together) extra_output = ["storageLayout", "metadata"] diff --git a/examples/simple/test/BadElections.t.sol b/examples/simple/test/BadElections.t.sol index 4bf89d96..24897f9c 100644 --- a/examples/simple/test/BadElections.t.sol +++ b/examples/simple/test/BadElections.t.sol @@ -18,12 +18,12 @@ contract BadElectionsTest is SymTest, Test { /// /// Running 1 tests for test/BadElections.t.sol:BadElectionsTest /// Counterexample: - /// halmos_fakeSig_bytes_01 = 0x00000000000000000000000000000000000000000000000000000000000000003fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a100 (65 bytes) + /// dolmos_fakeSig_bytes_01 = 0x00000000000000000000000000000000000000000000000000000000000000003fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a100 (65 bytes) /// p_proposalId_uint256 = 0x0000000000000000000000000000000000000000000000000000000000000000 (0) /// [FAIL] check_canNotVoteTwice(uint256) (paths: 7, time: 0.63s, bounds: []) /// /// the counterexample values are not meaningful, but examining the trace shows - /// that halmos found a signature s.t. the voter can vote twice on the same proposal, + /// that dolmos found a signature s.t. the voter can vote twice on the same proposal, /// and the final vote count is 2 function check_canNotVoteTwice(uint256 proposalId) public { // setup diff --git a/examples/simple/test/IsPowerOfTwo.t.sol b/examples/simple/test/IsPowerOfTwo.t.sol index 17e2ec57..b28820ab 100644 --- a/examples/simple/test/IsPowerOfTwo.t.sol +++ b/examples/simple/test/IsPowerOfTwo.t.sol @@ -3,7 +3,7 @@ pragma solidity >=0.8.0 <0.9.0; import "../src/IsPowerOfTwo.sol"; -/// @custom:halmos --solver-timeout-assertion 0 +/// @custom:dolmos --solver-timeout-assertion 0 contract IsPowerOfTwoTest { IsPowerOfTwo target; @@ -17,7 +17,7 @@ contract IsPowerOfTwoTest { assert(result1 == result2); } - /// @custom:halmos --loop 256 + /// @custom:dolmos --loop 256 function check_isPowerOfTwo(uint256 x) public view { bool result1 = target.isPowerOfTwo(x); bool result2 = false; @@ -30,7 +30,7 @@ contract IsPowerOfTwoTest { assert(result1 == result2); } - /// @custom:halmos --loop 256 + /// @custom:dolmos --loop 256 function check_eq_isPowerOfTwo_isPowerOfTwoIter(uint x) public view { bool result1 = target.isPowerOfTwo(x); bool result2 = target.isPowerOfTwoIter(x); diff --git a/examples/simple/test/TotalPrice.t.sol b/examples/simple/test/TotalPrice.t.sol index e1a6ed83..265b7063 100644 --- a/examples/simple/test/TotalPrice.t.sol +++ b/examples/simple/test/TotalPrice.t.sol @@ -3,7 +3,7 @@ pragma solidity >=0.8.0 <0.9.0; import "../src/TotalPrice.sol"; -/// @custom:halmos --solver-timeout-assertion 0 +/// @custom:dolmos --solver-timeout-assertion 0 contract TotalPriceTest { TotalPrice target; diff --git a/examples/simple/test/Vault.t.sol b/examples/simple/test/Vault.t.sol index d354ff10..583b5794 100644 --- a/examples/simple/test/Vault.t.sol +++ b/examples/simple/test/Vault.t.sol @@ -15,7 +15,7 @@ contract VaultMock is Vault { } } -/// @custom:halmos --solver-timeout-assertion 0 +/// @custom:dolmos --solver-timeout-assertion 0 contract VaultTest is SymTest { VaultMock vault; @@ -27,7 +27,7 @@ contract VaultTest is SymTest { } /// need to set a timeout for this test, the solver can run for hours - /// @custom:halmos --solver-timeout-assertion 10000 + /// @custom:dolmos --solver-timeout-assertion 10000 function check_deposit(uint assets) public { uint A1 = vault.totalAssets(); uint S1 = vault.totalShares(); diff --git a/examples/tokens/ERC20/test/BackdoorERC20.t.sol b/examples/tokens/ERC20/test/BackdoorERC20.t.sol index 43951486..b7a79867 100644 --- a/examples/tokens/ERC20/test/BackdoorERC20.t.sol +++ b/examples/tokens/ERC20/test/BackdoorERC20.t.sol @@ -10,10 +10,10 @@ import {IERC20} from "forge-std/interfaces/IERC20.sol"; // empty interface interface IEmpty { } -/// @custom:halmos --solver-timeout-assertion 0 +/// @custom:dolmos --solver-timeout-assertion 0 contract BackdoorERC20Test is ERC20Test { - /// @custom:halmos --solver-timeout-branching 1000 + /// @custom:dolmos --solver-timeout-branching 1000 function setUp() public override { address deployer = address(0x1000); diff --git a/examples/tokens/ERC20/test/CurveTokenV3.t.sol b/examples/tokens/ERC20/test/CurveTokenV3.t.sol index c68645c3..037cb9d3 100644 --- a/examples/tokens/ERC20/test/CurveTokenV3.t.sol +++ b/examples/tokens/ERC20/test/CurveTokenV3.t.sol @@ -28,12 +28,12 @@ interface CurveTokenV3 { contract EmptyContract { } -/// @custom:halmos --storage-layout=generic --solver-timeout-assertion 0 +/// @custom:dolmos --storage-layout=generic --solver-timeout-assertion 0 contract CurveTokenV3Test is ERC20Test { CurveTokenV3 token_; address minter; - /// @custom:halmos --solver-timeout-branching 1000 + /// @custom:dolmos --solver-timeout-branching 1000 function setUp() public override { token = address(new EmptyContract()); // Source of Deployed Bytecode: https://etherscan.io/address/0x06325440D014e39736583c165C2963BA99fAf14E#code diff --git a/examples/tokens/ERC20/test/DEIStablecoin.t.sol b/examples/tokens/ERC20/test/DEIStablecoin.t.sol index 942f9ed0..be29f5a0 100644 --- a/examples/tokens/ERC20/test/DEIStablecoin.t.sol +++ b/examples/tokens/ERC20/test/DEIStablecoin.t.sol @@ -19,12 +19,12 @@ contract SymAccount is SymTest { } /// @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 +/// @custom:dolmos --solver-timeout-assertion 0 contract DEIStablecoinTest is ERC20Test { DEIStablecoin token_; address lossless; - /// @custom:halmos --solver-timeout-branching 1000 + /// @custom:dolmos --solver-timeout-branching 1000 function setUp() public override { token = address(new EmptyContract()); // Source of Deployed Bytecode: https://etherscan.io/address/0x63c28e2ff796e1480eb9ac8c3c55dcb9ae7b3df6#code diff --git a/examples/tokens/ERC20/test/OpenZeppelinERC20.t.sol b/examples/tokens/ERC20/test/OpenZeppelinERC20.t.sol index d21913e7..dbdd4e77 100644 --- a/examples/tokens/ERC20/test/OpenZeppelinERC20.t.sol +++ b/examples/tokens/ERC20/test/OpenZeppelinERC20.t.sol @@ -5,10 +5,10 @@ import {ERC20Test} from "./ERC20Test.sol"; import {OpenZeppelinERC20} from "../src/OpenZeppelinERC20.sol"; -/// @custom:halmos --solver-timeout-assertion 0 +/// @custom:dolmos --solver-timeout-assertion 0 contract OpenZeppelinERC20Test is ERC20Test { - /// @custom:halmos --solver-timeout-branching 1000 + /// @custom:dolmos --solver-timeout-branching 1000 function setUp() public override { address deployer = address(0x1000); diff --git a/examples/tokens/ERC20/test/SoladyERC20.t.sol b/examples/tokens/ERC20/test/SoladyERC20.t.sol index 2c45da8c..57d87064 100644 --- a/examples/tokens/ERC20/test/SoladyERC20.t.sol +++ b/examples/tokens/ERC20/test/SoladyERC20.t.sol @@ -5,10 +5,10 @@ import {ERC20Test} from "./ERC20Test.sol"; import {SoladyERC20} from "../src/SoladyERC20.sol"; -/// @custom:halmos --storage-layout=generic --solver-timeout-assertion 0 +/// @custom:dolmos --storage-layout=generic --solver-timeout-assertion 0 contract SoladyERC20Test is ERC20Test { - /// @custom:halmos --solver-timeout-branching 1000 + /// @custom:dolmos --solver-timeout-branching 1000 function setUp() public override { address deployer = address(0x1000); diff --git a/examples/tokens/ERC20/test/SolmateERC20.t.sol b/examples/tokens/ERC20/test/SolmateERC20.t.sol index 6e64252e..89d453c5 100644 --- a/examples/tokens/ERC20/test/SolmateERC20.t.sol +++ b/examples/tokens/ERC20/test/SolmateERC20.t.sol @@ -5,10 +5,10 @@ import {ERC20Test} from "./ERC20Test.sol"; import {SolmateERC20} from "../src/SolmateERC20.sol"; -/// @custom:halmos --solver-timeout-assertion 0 +/// @custom:dolmos --solver-timeout-assertion 0 contract SolmateERC20Test is ERC20Test { - /// @custom:halmos --solver-timeout-branching 1000 + /// @custom:dolmos --solver-timeout-branching 1000 function setUp() public override { address deployer = address(0x1000); diff --git a/examples/tokens/ERC721/test/OpenZeppelinERC721.t.sol b/examples/tokens/ERC721/test/OpenZeppelinERC721.t.sol index 868a274d..26b9ed74 100644 --- a/examples/tokens/ERC721/test/OpenZeppelinERC721.t.sol +++ b/examples/tokens/ERC721/test/OpenZeppelinERC721.t.sol @@ -5,10 +5,10 @@ import {ERC721Test} from "./ERC721Test.sol"; import {OpenZeppelinERC721} from "../src/OpenZeppelinERC721.sol"; -/// @custom:halmos --solver-timeout-assertion 0 +/// @custom:dolmos --solver-timeout-assertion 0 contract OpenZeppelinERC721Test is ERC721Test { - /// @custom:halmos --solver-timeout-branching 1000 + /// @custom:dolmos --solver-timeout-branching 1000 function setUp() public override { deployer = address(0x1000); diff --git a/examples/tokens/ERC721/test/SoladyERC721.t.sol b/examples/tokens/ERC721/test/SoladyERC721.t.sol index c3241a5e..72f62e26 100644 --- a/examples/tokens/ERC721/test/SoladyERC721.t.sol +++ b/examples/tokens/ERC721/test/SoladyERC721.t.sol @@ -5,10 +5,10 @@ import {ERC721Test} from "./ERC721Test.sol"; import {SoladyERC721} from "../src/SoladyERC721.sol"; -/// @custom:halmos --storage-layout=generic --solver-timeout-assertion 0 +/// @custom:dolmos --storage-layout=generic --solver-timeout-assertion 0 contract SoladyERC721Test is ERC721Test { - /// @custom:halmos --solver-timeout-branching 1000 + /// @custom:dolmos --solver-timeout-branching 1000 function setUp() public override { deployer = address(0x1000); diff --git a/examples/tokens/ERC721/test/SolmateERC721.t.sol b/examples/tokens/ERC721/test/SolmateERC721.t.sol index 1ca6eb4f..8e3f256e 100644 --- a/examples/tokens/ERC721/test/SolmateERC721.t.sol +++ b/examples/tokens/ERC721/test/SolmateERC721.t.sol @@ -5,10 +5,10 @@ import {ERC721Test} from "./ERC721Test.sol"; import {SolmateERC721} from "../src/SolmateERC721.sol"; -/// @custom:halmos --solver-timeout-assertion 0 +/// @custom:dolmos --solver-timeout-assertion 0 contract SolmateERC721Test is ERC721Test { - /// @custom:halmos --solver-timeout-branching 1000 + /// @custom:dolmos --solver-timeout-branching 1000 function setUp() public override { deployer = address(0x1000); diff --git a/packages/halmos-builder/Dockerfile b/packages/dolmos-builder/Dockerfile similarity index 92% rename from packages/halmos-builder/Dockerfile rename to packages/dolmos-builder/Dockerfile index b6259468..ac6d991f 100644 --- a/packages/halmos-builder/Dockerfile +++ b/packages/dolmos-builder/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/a16z/solvers:latest +FROM ghcr.io/dowsers/solvers:latest RUN apt-get update && apt-get install --no-install-recommends -y \ git \ @@ -25,5 +25,5 @@ RUN curl -sSf -L https://raw.githubusercontent.com/foundry-rs/foundry/master/fou foundryup # Set a nicer prompt -ENV IMAGE_NAME=halmos-builder +ENV IMAGE_NAME=dolmos-builder RUN echo 'PS1="($IMAGE_NAME) \[\033[1;32m\]\u@\h \[\033[1;35m\]\w \$\[\033[0m\] "' >> /root/.bashrc diff --git a/packages/halmos/Dockerfile b/packages/dolmos/Dockerfile similarity index 70% rename from packages/halmos/Dockerfile rename to packages/dolmos/Dockerfile index 1a26a55d..3e041bb8 100644 --- a/packages/halmos/Dockerfile +++ b/packages/dolmos/Dockerfile @@ -1,36 +1,36 @@ -FROM ghcr.io/a16z/halmos-builder:latest +FROM ghcr.io/dowsers/dolmos-builder:latest # inspired by https://hynek.me/articles/docker-uv/ # - enable the virtual environment -# - install halmos and its dependencies in UV_PROJECT_ENVIRONMENT +# - install dolmos and its dependencies in UV_PROJECT_ENVIRONMENT # - enable bytecode compilation for faster startup # - disable downloading any additional packages # - use copy mode for linking instead of symlinking (because we mount to /src temporarily) -ENV PATH="/halmos/bin:$PATH" \ - VIRTUAL_ENV='/halmos' \ - UV_PROJECT_ENVIRONMENT='/halmos' \ +ENV PATH="/dolmos/bin:$PATH" \ + VIRTUAL_ENV='/dolmos' \ + UV_PROJECT_ENVIRONMENT='/dolmos' \ UV_COMPILE_BYTECODE=1 \ UV_PYTHON=python3.13 \ UV_PYTHON_DOWNLOADS=never \ UV_LINK_MODE=copy -# Install halmos, assuming it is checked out in the current host directory +# Install dolmos, assuming it is checked out in the current host directory # we don't specify --frozen or --locked because we don't check in uv.lock RUN --mount=type=bind,source=../..,target=/src,readonly=false \ cd /src && \ uv sync --extra dev --no-editable # Set a nicer prompt -ENV IMAGE_NAME=halmos +ENV IMAGE_NAME=dolmos RUN echo 'PS1="($IMAGE_NAME) \[\033[1;32m\]\u@\h \[\033[1;35m\]\w \$\[\033[0m\] "' >> /root/.bashrc -# optional: print python version, site packages, and check that halmos can be imported +# optional: print python version, site packages, and check that dolmos can be imported RUN < HalmosConfig: +def with_devdoc(args: DolmosConfig, fn_sig: str, contract_json: dict) -> DolmosConfig: devdoc = parse_devdoc(fn_sig, contract_json) if not devdoc: return args @@ -202,8 +203,8 @@ def with_devdoc(args: HalmosConfig, fn_sig: str, contract_json: dict) -> HalmosC def with_natspec( - args: HalmosConfig, contract_name: str, contract_natspec: str -) -> HalmosConfig: + args: DolmosConfig, contract_name: str, contract_natspec: str +) -> DolmosConfig: if not contract_natspec: return args @@ -216,7 +217,7 @@ def with_natspec( return args.with_overrides(source, **vars(overrides)) -def with_vyper_defaults(args: HalmosConfig) -> HalmosConfig: +def with_vyper_defaults(args: DolmosConfig) -> DolmosConfig: """ Adjust the defaults for projects that contain vyper contracts: - the solidity storage layout model does not apply to vyper (e.g. HashMap slots @@ -250,7 +251,7 @@ def with_vyper_defaults(args: HalmosConfig) -> HalmosConfig: return args.with_overrides(ConfigSource.default, **overrides) -def load_config(_args) -> HalmosConfig: +def load_config(_args) -> DolmosConfig: config = default_config() # parse CLI args first, so that can get `--help` out of the way and resolve `--debug` @@ -293,7 +294,7 @@ def mk_addr(name: str) -> Address: return BitVec(name, 160) -def mk_solver(args: HalmosConfig, logic="QF_AUFBV", ctx=None) -> Solver: +def mk_solver(args: DolmosConfig, logic="QF_AUFBV", ctx=None) -> Solver: # in the config, we have a float in seconds # z3 expects an int in milliseconds timeout_ms = int(args.solver_timeout_branching * 1000) @@ -352,7 +353,7 @@ def deploy_test(ctx: FunctionContext, sevm: SEVM) -> Exec: # sanity check if len(exs) != 1: - raise HalmosException(f"constructor: # of paths: {len(exs)}") + raise DolmosException(f"constructor: # of paths: {len(exs)}") [ex] = exs @@ -366,7 +367,7 @@ def deploy_test(ctx: FunctionContext, sevm: SEVM) -> Exec: output = ex.context.output returndata = output.data if output.error is not None or not returndata: - raise HalmosException(f"constructor failed: {output.error=} {returndata=}") + raise DolmosException(f"constructor failed: {output.error=} {returndata=}") deployed_bytecode = Contract(returndata) ex.try_resolve_contract_info(deployed_bytecode, args.coverage_output) @@ -463,10 +464,10 @@ def setup(ctx: FunctionContext) -> Exec: match len(setup_exs): case 0: - raise HalmosException(f"No successful path found in {setup_sig}") + raise DolmosException(f"No successful path found in {setup_sig}") case n if n > 1: debug("\n".join(map(str, setup_exs))) - raise HalmosException(f"Multiple paths were found in {setup_sig}") + raise DolmosException(f"Multiple paths were found in {setup_sig}") [setup_ex] = setup_exs @@ -526,7 +527,7 @@ def get_state_id(ex: Exec) -> bytes: def run_target_function( - args: HalmosConfig, + args: DolmosConfig, ex: Exec, addr: Address, abi: dict, @@ -614,12 +615,12 @@ def run_target_contract( try: # create a symbolic tx.origin tx_origin = mk_addr( - f"halmos_tx_origin_{id_str(addr)}_{uid()}_{ex.new_symbol_id():>02}" + f"dolmos_tx_origin_{id_str(addr)}_{uid()}_{ex.new_symbol_id():>02}" ) # create a symbolic msg.sender msg_sender = mk_addr( - f"halmos_msg_sender_{id_str(addr)}_{uid()}_{ex.new_symbol_id():>02}" + f"dolmos_msg_sender_{id_str(addr)}_{uid()}_{ex.new_symbol_id():>02}" ) # restrict msg.sender to the specified target senders. @@ -637,7 +638,7 @@ def run_target_contract( # create a symbolic msg.value msg_value = BitVec( - f"halmos_msg_value_{id_str(addr)}_{uid()}_{ex.new_symbol_id():>02}", + f"dolmos_msg_value_{id_str(addr)}_{uid()}_{ex.new_symbol_id():>02}", BitVecSort256, ) @@ -736,7 +737,7 @@ def _compute_frontier(ctx: ContractContext, depth: int) -> Iterator[Exec]: if flamegraph_enabled: call_flamegraph.add_with_sequence(post_ex.call_sequence, subcall) - # ignore and report if halmos-errored + # ignore and report if dolmos-errored if subcall.is_stuck(): error( f"{depth=}: addr={hexify(addr)}: {subcall.get_stuck_reason()}" @@ -796,7 +797,7 @@ def _compute_frontier(ctx: ContractContext, depth: int) -> Iterator[Exec]: visited.add(post_id) # update timestamp - timestamp_name = f"halmos_block_timestamp_depth{depth}_{uid()}" + timestamp_name = f"dolmos_block_timestamp_depth{depth}_{uid()}" post_ex.block.timestamp = ZeroExt(192, BitVec(timestamp_name, 64)) post_ex.path.append(post_ex.block.timestamp >= pre_ex.block.timestamp) @@ -1093,7 +1094,7 @@ def run_test(ctx: FunctionContext) -> TestResult: is_invariant = funname.startswith("invariant_") # set the config for every trace rendered in this test - halmos.traces.config_context.set(args) + dolmos.traces.config_context.set(args) # # prepare calldata @@ -1403,7 +1404,7 @@ def execute_simple_getter( # sanity check if len(exs) != 1: - raise HalmosException(f"{fun_info.sig}: # of paths: {len(exs)}") + raise DolmosException(f"{fun_info.sig}: # of paths: {len(exs)}") [ex] = exs @@ -1411,7 +1412,7 @@ def execute_simple_getter( returndata = output.data if output.error is not None or not returndata: - raise HalmosException(f"{fun_info.sig}: {output.error=} {returndata=}") + raise DolmosException(f"{fun_info.sig}: {output.error=} {returndata=}") return returndata @@ -1575,7 +1576,7 @@ def resolve_target_contracts(ctx: InvariantTestingContext, ex: Exec) -> set[Addr ) if not resolved_target_contracts: - raise HalmosException("No target contracts available.") + raise DolmosException("No target contracts available.") return resolved_target_contracts @@ -1611,7 +1612,7 @@ def resolve_target_selectors( debug(f"Skipping {fun_sig} ({state_mutability})") continue - # https://github.com/a16z/halmos/issues/514 + # see upstream halmos issue #514 # exclude special functions like test_, check_, setUp(), etc. if is_test_contract and ( fun_sig.startswith("test_") @@ -1648,7 +1649,7 @@ def run_contract(ctx: ContractContext) -> list[TestResult]: contract_ctx=ctx, ) - halmos.traces.config_context.set(setup_config) + dolmos.traces.config_context.set(setup_config) setup_ex = setup(setup_ctx) setup_ex.path_slice() except Exception as err: @@ -1676,7 +1677,7 @@ def run_contract(ctx: ContractContext) -> list[TestResult]: def print_invariant_targets(ctx: ContractContext, ex: Exec): try: target_contracts = resolve_target_contracts(ctx.inv_ctx, ex) - except HalmosException: + except DolmosException: # no target contracts found, let the downstream tests fail target_contracts = [] @@ -1817,10 +1818,7 @@ def _main(_args=None) -> MainResult: args = load_config(_args) if args.version: - # the distribution is dolmos, the importable package is still halmos - print( - f"{os.path.basename(sys.argv[0]) or 'dolmos'} {metadata.version('dolmos')}" - ) + print(f"{program_name()} {metadata.version('dolmos')}") return MainResult(0) init_env(args.root) @@ -1833,7 +1831,7 @@ def _main(_args=None) -> MainResult: logger_unique.setLevel(logging.DEBUG) if args.trace_memory: - import halmos.memtrace as memtrace + import dolmos.memtrace as memtrace memtrace.MemTracer.get().start() @@ -1963,7 +1961,7 @@ def on_signal(signum, frame): # Set the test contract address in DeployAddressMapper DeployAddressMapper().add_deployed_contract(hexify(FOUNDRY_TEST), contract_name) - # support for `/// @custom:halmos` annotations + # support for `/// @custom:dolmos` annotations contract_args = with_natspec(args, contract_name, natspec) contract_ctx = ContractContext( args=contract_args, @@ -2037,12 +2035,12 @@ def on_signal(signum, frame): return on_exit(exitcode) -# entrypoint for the `halmos` script +# entrypoint for the `dolmos` script def main() -> int: exitcode = _main().exitcode return exitcode -# entrypoint for `python -m halmos` +# entrypoint for `python -m dolmos` if __name__ == "__main__": sys.exit(main()) diff --git a/src/halmos/assertions.py b/src/dolmos/assertions.py similarity index 98% rename from src/halmos/assertions.py rename to src/dolmos/assertions.py index de9e432e..b4a22556 100644 --- a/src/halmos/assertions.py +++ b/src/dolmos/assertions.py @@ -13,8 +13,8 @@ is_bv, ) -from halmos.exceptions import HalmosException -from halmos.utils import ( +from dolmos.exceptions import DolmosException +from dolmos.utils import ( bytes_to_bv_value, extract_bytes, extract_bytes32_array_argument, @@ -178,7 +178,7 @@ def mk_assert_handler(signature): operator = match.group(1) params = match.group(2).split(",") else: - raise HalmosException(f"not supported signatures: {signature}") + raise DolmosException(f"not supported signatures: {signature}") # operator type: # - binary: compare two arguments diff --git a/src/halmos/bitvec.py b/src/dolmos/bitvec.py similarity index 71% rename from src/halmos/bitvec.py rename to src/dolmos/bitvec.py index 1c0f4a6d..cff22de4 100644 --- a/src/halmos/bitvec.py +++ b/src/dolmos/bitvec.py @@ -34,10 +34,10 @@ simplify, ) -from halmos.exceptions import NotConcreteError +from dolmos.exceptions import NotConcreteError -BV: TypeAlias = "HalmosBitVec" -AnyValue: TypeAlias = "int | bytes | BitVecRef | BV | HalmosBool | str" +BV: TypeAlias = "DolmosBitVec" +AnyValue: TypeAlias = "int | bytes | BitVecRef | BV | DolmosBool | str" BVValue: TypeAlias = int | BitVecRef | BoolRef MaybeSize: TypeAlias = int | None AnyBool: TypeAlias = bool | BoolRef @@ -62,11 +62,11 @@ def as_int(value: AnyValue) -> tuple[BVValue, MaybeSize]: return int.from_bytes(value, "big"), len(value) * 8 elif is_bv_value(value): return value.as_long(), value.size() - elif isinstance(value, HalmosBitVec): + elif isinstance(value, DolmosBitVec): return value.unwrap(), value.size elif isinstance(value, BitVecRef): return value, value.size() - elif isinstance(value, HalmosBool): + elif isinstance(value, DolmosBool): return value.unwrap(), 1 elif isinstance(value, BoolRef): return value, 1 @@ -74,23 +74,23 @@ def as_int(value: AnyValue) -> tuple[BVValue, MaybeSize]: raise TypeError(f"Cannot convert {type(value)} to int") -class HalmosBool: +class DolmosBool: """ Immutable wrapper for concrete or symbolic boolean values. Can be constructed with: - - HalmosBool(42 % 2 == 0) # bool - - HalmosBool(BitVec(x, 8) != 0) # BoolRef - - HalmosBool(HalmosBitVec(x, 8)) # HalmosBitVec, same as above + - DolmosBool(42 % 2 == 0) # bool + - DolmosBool(BitVec(x, 8) != 0) # BoolRef + - DolmosBool(DolmosBitVec(x, 8)) # DolmosBitVec, same as above - Conversion to and from HalmosBitVec: - - HalmosBitVec(halmos_bool) (same as halmos_bool.as_bv()) - - HalmosBool(halmos_bitvec) (same as halmos_bitvec.is_non_zero()) + Conversion to and from DolmosBitVec: + - DolmosBitVec(dolmos_bool) (same as dolmos_bool.as_bv()) + - DolmosBool(dolmos_bitvec) (same as dolmos_bitvec.is_non_zero()) Wrapping/unwrapping: - - halmos_bool.as_z3() always converts to a z3 BoolRef - - bool(halmos_bool) converts to a concrete bool, raises if symbolic - - halmos_bool.value/unwrap() returns the underlying bool or z3 BoolRef + - dolmos_bool.as_z3() always converts to a z3 BoolRef + - bool(dolmos_bool) converts to a concrete bool, raises if symbolic + - dolmos_bool.value/unwrap() returns the underlying bool or z3 BoolRef """ __slots__ = ("con_val", "sym_val") @@ -103,10 +103,10 @@ def __new__(cls, value, *, do_simplify: bool = True): if type_value is bool: return TRUE if value else FALSE - if type_value is HalmosBool: + if type_value is DolmosBool: return value - if type_value is HalmosBitVec: + if type_value is DolmosBitVec: return value.is_non_zero() if type_value is BoolRef: @@ -137,14 +137,14 @@ def __init__(self, value: AnyBool | str, *, do_simplify: bool = True): self.sym_val = Bool(value) self.con_val = None - case HalmosBool(): + case DolmosBool(): return - case HalmosBitVec(): + case DolmosBitVec(): return case _: - raise TypeError(f"Cannot create HalmosBool from {type(value)}") + raise TypeError(f"Cannot create DolmosBool from {type(value)}") assert self.con_val is None or self.sym_val is None assert self.con_val is not None or self.sym_val is not None @@ -209,7 +209,7 @@ def __eq__(self, other: Any) -> bool: note: this is not the same as z3's comparison, which returns a constraint """ - if not isinstance(other, HalmosBool): + if not isinstance(other, DolmosBool): return False match (self.con_val, other.con_val): @@ -232,28 +232,28 @@ def __hash__(self) -> int: return hash((self.con_val, self.sym_val)) - def is_zero(self) -> "HalmosBool": + def is_zero(self) -> "DolmosBool": if self is TRUE: return FALSE if self is FALSE: return TRUE - return HalmosBool(Not(self.sym_val)) + return DolmosBool(Not(self.sym_val)) - def is_non_zero(self) -> "HalmosBool": + def is_non_zero(self) -> "DolmosBool": return self - def eq(self, other: "HalmosBool") -> "HalmosBool": - return HalmosBool(self.value == other.value) + def eq(self, other: "DolmosBool") -> "DolmosBool": + return DolmosBool(self.value == other.value) - def neg(self) -> "HalmosBool": + def neg(self) -> "DolmosBool": return self.is_zero() - def bitwise_not(self) -> "HalmosBool": + def bitwise_not(self) -> "DolmosBool": return self.is_zero() - def bitwise_and(self, other: "HalmosBool") -> "HalmosBool": + def bitwise_and(self, other: "DolmosBool") -> "DolmosBool": if self is TRUE: return other @@ -266,9 +266,9 @@ def bitwise_and(self, other: "HalmosBool") -> "HalmosBool": if other is FALSE: return other - return HalmosBool(And(self.as_z3(), other.as_z3())) + return DolmosBool(And(self.as_z3(), other.as_z3())) - def bitwise_or(self, other: "HalmosBool") -> "HalmosBool": + def bitwise_or(self, other: "DolmosBool") -> "DolmosBool": if self is TRUE: return self @@ -281,9 +281,9 @@ def bitwise_or(self, other: "HalmosBool") -> "HalmosBool": if other is FALSE: return self - return HalmosBool(Or(self.as_z3(), other.as_z3())) + return DolmosBool(Or(self.as_z3(), other.as_z3())) - def bitwise_xor(self, other: "HalmosBool") -> "HalmosBool": + def bitwise_xor(self, other: "DolmosBool") -> "DolmosBool": if self is TRUE: return other.bitwise_not() @@ -296,47 +296,47 @@ def bitwise_xor(self, other: "HalmosBool") -> "HalmosBool": if other is FALSE: return self - return HalmosBool(Xor(self.as_z3(), other.as_z3())) + return DolmosBool(Xor(self.as_z3(), other.as_z3())) def as_bv(self, size: int = 1) -> BV: if self is TRUE: - return ONE if size == 256 else HalmosBitVec(1, size=size) + return ONE if size == 256 else DolmosBitVec(1, size=size) if self is FALSE: - return ZERO if size == 256 else HalmosBitVec(0, size=size) + return ZERO if size == 256 else DolmosBitVec(0, size=size) expr = If(self.sym_val, BitVecVal(1, size), BitVecVal(0, size)) - return HalmosBitVec(expr, size=size) + return DolmosBitVec(expr, size=size) -class HalmosBitVec: +class DolmosBitVec: """ Immutable wrapper for concrete or symbolic bitvectors. Can be constructed with: - - HalmosBitVec(42) # int, size is default (256) - - HalmosBitVec(-1, size=8) # int, 2's complement - - HalmosBitVec(bytes.fromhex("12345678")) # bytes, size is inherited - - HalmosBitVec(BitVecVal(42, 8)) # BitVecVal, size is inherited - - HalmosBitVec(BitVec(x, 8)) # BitVecRef, size is inherited - - HalmosBitVec(halmos_bool) # HalmosBool, size 1 - - HalmosBitVec("x") # named symbol, size is default (256) - - Conversion to and from HalmosBool: - - HalmosBitVec(halmos_bool) (same as halmos_bool.as_bv()) - - HalmosBool(halmos_bitvec) (same as halmos_bitvec.is_non_zero()) + - DolmosBitVec(42) # int, size is default (256) + - DolmosBitVec(-1, size=8) # int, 2's complement + - DolmosBitVec(bytes.fromhex("12345678")) # bytes, size is inherited + - DolmosBitVec(BitVecVal(42, 8)) # BitVecVal, size is inherited + - DolmosBitVec(BitVec(x, 8)) # BitVecRef, size is inherited + - DolmosBitVec(dolmos_bool) # DolmosBool, size 1 + - DolmosBitVec("x") # named symbol, size is default (256) + + Conversion to and from DolmosBool: + - DolmosBitVec(dolmos_bool) (same as dolmos_bool.as_bv()) + - DolmosBool(dolmos_bitvec) (same as dolmos_bitvec.is_non_zero()) Wrapping/unwrapping: - - halmos_bitvec.as_z3() always converts to a z3 BitVecRef - - int(halmos_bitvec) converts to a concrete int, raises if symbolic - - halmos_bitvec.value/unwrap() returns the underlying int or z3 BitVecRef + - dolmos_bitvec.as_z3() always converts to a z3 BitVecRef + - int(dolmos_bitvec) converts to a concrete int, raises if symbolic + - dolmos_bitvec.value/unwrap() returns the underlying int or z3 BitVecRef """ __slots__ = ("_value", "_symbolic", "_size") def __new__(cls, value, size=None, do_simplify=True): - # fast path for existing HalmosBitVec of same size - if isinstance(value, HalmosBitVec): + # fast path for existing DolmosBitVec of same size + if isinstance(value, DolmosBitVec): size = size or value._size if size == value._size: return value @@ -348,7 +348,7 @@ def __init__( self, value: AnyValue, *, size: MaybeSize = None, do_simplify: bool = True ): """ - Create a new HalmosBitVec from an integer, bytes, or a z3 BitVecRef. + Create a new DolmosBitVec from an integer, bytes, or a z3 BitVecRef. If the value is too large for the bit size, it will be truncated. @@ -357,16 +357,16 @@ def __init__( type_value = type(value) - if type_value is HalmosBitVec: - # avoid reinitializing HalmosBitVec because of __new__ shortcut if same size + if type_value is DolmosBitVec: + # avoid reinitializing DolmosBitVec because of __new__ shortcut if same size if size == value.size: return - # otherwise, create a new HalmosBitVec with the new size + # otherwise, create a new DolmosBitVec with the new size value = value.unwrap() - # unwrap HalmosBool - elif type_value is HalmosBool: + # unwrap DolmosBool + elif type_value is DolmosBool: value = value.unwrap() # convenience, wrap a string as a named symbol @@ -404,7 +404,7 @@ def __init__( self._value = simplified else: - raise TypeError(f"Cannot create HalmosBitVec from {type(value)}") + raise TypeError(f"Cannot create DolmosBitVec from {type(value)}") def __deepcopy__(self, memo): # yay immutability @@ -441,7 +441,7 @@ def __eq__(self, other: Any) -> bool: note: this is not the same as z3's comparison, which returns a constraint """ - if not isinstance(other, HalmosBitVec): + if not isinstance(other, DolmosBitVec): return False if self._size != other.size: @@ -476,13 +476,13 @@ def __repr__(self) -> str: def __str__(self) -> str: return str(self._value) - def is_zero(self) -> HalmosBool: + def is_zero(self) -> DolmosBool: # works for both symbolic and concrete - return HalmosBool(self._value == 0) + return DolmosBool(self._value == 0) - def is_non_zero(self) -> HalmosBool: + def is_non_zero(self) -> DolmosBool: # works for both symbolic and concrete - return HalmosBool(self._value != 0) + return DolmosBool(self._value != 0) # # operations @@ -492,24 +492,24 @@ def add(self, other: BV) -> BV: size = self._size assert size == other._size - return HalmosBitVec(self._value + other._value, size=size) + return DolmosBitVec(self._value + other._value, size=size) - def sub(self, other: BV) -> "HalmosBitVec": + def sub(self, other: BV) -> "DolmosBitVec": size = self._size assert size == other._size - return HalmosBitVec(self._value - other._value, size=size) + return DolmosBitVec(self._value - other._value, size=size) def mul( self, other: BV, *, abstraction: FuncDeclRef | None = None - ) -> "HalmosBitVec": + ) -> "DolmosBitVec": size = self._size assert size == other.size lhs, rhs = self.value, other.value match (self.is_concrete, other.is_concrete): case (True, True): - return HalmosBitVec(lhs * rhs, size=size) + return DolmosBitVec(lhs * rhs, size=size) case (True, False): if lhs == 0: @@ -519,9 +519,9 @@ def mul( return other if is_power_of_two(lhs): - return other.lshl(HalmosBitVec(lhs.bit_length() - 1, size=size)) + return other.lshl(DolmosBitVec(lhs.bit_length() - 1, size=size)) - return HalmosBitVec(lhs * rhs, size=size) + return DolmosBitVec(lhs * rhs, size=size) case (False, True): if rhs == 0: @@ -531,20 +531,20 @@ def mul( return self if is_power_of_two(rhs): - return self.lshl(HalmosBitVec(rhs.bit_length() - 1, size=size)) + return self.lshl(DolmosBitVec(rhs.bit_length() - 1, size=size)) # simplify(x * 123) -> 123 * x - return HalmosBitVec(rhs * lhs, size=size) + return DolmosBitVec(rhs * lhs, size=size) return ( - HalmosBitVec(lhs * rhs, size=size) + DolmosBitVec(lhs * rhs, size=size) if abstraction is None - else HalmosBitVec(abstraction(lhs, rhs), size=size) + else DolmosBitVec(abstraction(lhs, rhs), size=size) ) def div( self, other: BV, *, abstraction: FuncDeclRef | None = None - ) -> "HalmosBitVec": + ) -> "DolmosBitVec": # TODO: div_xy_y size = self._size @@ -564,20 +564,20 @@ def div( # fully concrete case if self.is_concrete: - return HalmosBitVec(lhs // rhs, size=size) + return DolmosBitVec(lhs // rhs, size=size) if is_power_of_two(rhs): - return self.lshr(HalmosBitVec(rhs.bit_length() - 1, size=size)) + return self.lshr(DolmosBitVec(rhs.bit_length() - 1, size=size)) # symbolic case if abstraction is None: - return HalmosBitVec(UDiv(lhs, rhs), size=size) + return DolmosBitVec(UDiv(lhs, rhs), size=size) - return HalmosBitVec(abstraction(lhs, rhs), size=size) + return DolmosBitVec(abstraction(lhs, rhs), size=size) def sdiv( self, other: BV, *, abstraction: FuncDeclRef | None = None - ) -> "HalmosBitVec": + ) -> "DolmosBitVec": # TODO: sdiv_xy_y size = self._size assert size == other.size @@ -593,20 +593,20 @@ def sdiv( if self.is_concrete: # rely on z3 to handle the signed division - return HalmosBitVec( + return DolmosBitVec( BitVecVal(lhs, size) / BitVecVal(rhs, size), size=size, do_simplify=True, ) if abstraction is None: - return HalmosBitVec(other / self, size=size) + return DolmosBitVec(other / self, size=size) - return HalmosBitVec(abstraction(lhs, rhs), size=size) + return DolmosBitVec(abstraction(lhs, rhs), size=size) def mod( - self, other: "HalmosBitVec", *, abstraction: FuncDeclRef | None = None - ) -> "HalmosBitVec": + self, other: "DolmosBitVec", *, abstraction: FuncDeclRef | None = None + ) -> "DolmosBitVec": size = self._size assert size == other.size @@ -619,27 +619,27 @@ def mod( # mod by one is zero if rhs == 1: - return HalmosBitVec(0, size=size) + return DolmosBitVec(0, size=size) # fully concrete case if self.is_concrete: - return HalmosBitVec(lhs % rhs, size=size) + return DolmosBitVec(lhs % rhs, size=size) # mod by a power of two only keeps the low bits if is_power_of_two(rhs): # option 1: use z3 bitwise and # 10000 loops, best of 5: 148.215 usec per loop - # return HalmosBitVec(lhs & (rhs - 1), size=size) + # return DolmosBitVec(lhs & (rhs - 1), size=size) # option 2: truncate and extend # 10000 loops, best of 5: 146.525 usec per loop - # truncated = HalmosBitVec(lhs, size=rhs.bit_length() - 1) - # return HalmosBitVec(truncated, size=size) + # truncated = DolmosBitVec(lhs, size=rhs.bit_length() - 1) + # return DolmosBitVec(truncated, size=size) # option 3: truncate and extend, skip simplify # 10000 loops, best of 5: 130.028 usec per loop - # truncated = HalmosBitVec(lhs, size=rhs.bit_length() - 1, do_simplify=False) - # return HalmosBitVec(truncated, size=size) + # truncated = DolmosBitVec(lhs, size=rhs.bit_length() - 1, do_simplify=False) + # return DolmosBitVec(truncated, size=size) # option 4: explicit ZeroExt and Extract # on this path, we know that rhs > 1, so bitsize > 1 @@ -647,17 +647,17 @@ def mod( bitsize = rhs.bit_length() - 1 truncated = Extract(bitsize - 1, 0, lhs) extended = ZeroExt(size - bitsize, truncated) - return HalmosBitVec(extended, size=size) + return DolmosBitVec(extended, size=size) # symbolic case if abstraction is None: - return HalmosBitVec(URem(lhs, rhs), size=size) + return DolmosBitVec(URem(lhs, rhs), size=size) - return HalmosBitVec(abstraction(lhs, rhs), size=size) + return DolmosBitVec(abstraction(lhs, rhs), size=size) def smod( - self, other: "HalmosBitVec", *, abstraction: FuncDeclRef | None = None - ) -> "HalmosBitVec": + self, other: "DolmosBitVec", *, abstraction: FuncDeclRef | None = None + ) -> "DolmosBitVec": size = self._size assert size == other.size @@ -669,14 +669,14 @@ def smod( # mod by one is zero if rhs == 1: - return HalmosBitVec(0, size=size) + return DolmosBitVec(0, size=size) # TODO: implement concrete signed remainder # (tricky because it truncates towards zero) if self.is_concrete: # rely on z3 to handle the signed division - return HalmosBitVec( + return DolmosBitVec( SRem(BitVecVal(lhs, size), BitVecVal(rhs, size)), size=size, do_simplify=True, @@ -684,18 +684,18 @@ def smod( # lhs and rhs are symbolic if abstraction is None: - return HalmosBitVec(SRem(lhs, rhs), size=size) + return DolmosBitVec(SRem(lhs, rhs), size=size) - return HalmosBitVec(abstraction(lhs, rhs), size=size) + return DolmosBitVec(abstraction(lhs, rhs), size=size) def exp( self, - other: "HalmosBitVec", + other: "DolmosBitVec", *, exp_abstraction: FuncDeclRef | None = None, mul_abstraction: FuncDeclRef | None = None, smt_exp_by_const: int = 0, - ) -> "HalmosBitVec": + ) -> "DolmosBitVec": size = self._size assert size == other.size @@ -703,13 +703,13 @@ def exp( if other.is_concrete: if rhs == 0: - return HalmosBitVec(1, size=size) + return DolmosBitVec(1, size=size) if rhs == 1: return self if self.is_concrete: - return HalmosBitVec(lhs**rhs, size=size) + return DolmosBitVec(lhs**rhs, size=size) if rhs <= smt_exp_by_const: exp = self @@ -720,55 +720,55 @@ def exp( if exp_abstraction is None: raise NotImplementedError("missing SMT abstraction for exponentiation") - return HalmosBitVec(exp_abstraction(lhs, rhs), size=size) + return DolmosBitVec(exp_abstraction(lhs, rhs), size=size) def addmod( self, - other: "HalmosBitVec", - modulus: "HalmosBitVec", + other: "DolmosBitVec", + modulus: "DolmosBitVec", *, abstraction: FuncDeclRef | None = None, - ) -> "HalmosBitVec": + ) -> "DolmosBitVec": size = self._size assert size == other.size assert size == modulus.size if self.is_concrete and other.is_concrete and modulus.is_concrete: - return HalmosBitVec((self.value + other.value) % modulus.value, size=size) + return DolmosBitVec((self.value + other.value) % modulus.value, size=size) # to avoid add overflow; and to be a multiple of 8-bit newsize = size + 8 - r1 = HalmosBitVec(self, size=newsize).add(HalmosBitVec(other, size=newsize)) - r2 = r1.mod(HalmosBitVec(modulus, size=newsize), abstraction=abstraction) + r1 = DolmosBitVec(self, size=newsize).add(DolmosBitVec(other, size=newsize)) + r2 = r1.mod(DolmosBitVec(modulus, size=newsize), abstraction=abstraction) if r1.size != newsize: raise ValueError(r1) if r2.size != newsize: raise ValueError(r2) - return HalmosBitVec(r2, size=size) + return DolmosBitVec(r2, size=size) def mulmod( self, - other: "HalmosBitVec", - modulus: "HalmosBitVec", + other: "DolmosBitVec", + modulus: "DolmosBitVec", *, mul_abstraction: FuncDeclRef | None = None, mod_abstraction: FuncDeclRef | None = None, - ) -> "HalmosBitVec": + ) -> "DolmosBitVec": size = self._size assert size == other.size assert size == modulus.size if self.is_concrete and other.is_concrete and modulus.is_concrete: - return HalmosBitVec((self.value * other.value) % modulus.value, size=size) + return DolmosBitVec((self.value * other.value) % modulus.value, size=size) # to avoid mul overflow newsize = size * 2 - self_ext = HalmosBitVec(self, size=newsize) - other_ext = HalmosBitVec(other, size=newsize) - mod_ext = HalmosBitVec(modulus, size=newsize) + self_ext = DolmosBitVec(self, size=newsize) + other_ext = DolmosBitVec(other, size=newsize) + mod_ext = DolmosBitVec(modulus, size=newsize) r1 = self_ext.mul(other_ext, abstraction=mul_abstraction) r2 = r1.mod(mod_ext, abstraction=mod_abstraction) @@ -779,9 +779,9 @@ def mulmod( if r2.size != newsize: raise ValueError(r2) - return HalmosBitVec(r2, size=size) + return DolmosBitVec(r2, size=size) - def signextend(self, size: int) -> "HalmosBitVec": + def signextend(self, size: int) -> "DolmosBitVec": # TODO: handle other sizes assert self.size == 256 @@ -791,11 +791,11 @@ def signextend(self, size: int) -> "HalmosBitVec": # TODO: handle concrete case natively bl = (size + 1) * 8 - return HalmosBitVec( + return DolmosBitVec( SignExt(256 - bl, Extract(bl - 1, 0, self.as_z3())), size=256 ) - def lshl(self, shift: BV) -> "HalmosBitVec": + def lshl(self, shift: BV) -> "DolmosBitVec": """ Logical left shift """ @@ -808,11 +808,11 @@ def lshl(self, shift: BV) -> "HalmosBitVec": return self if shift_amount >= size: - return HalmosBitVec(0, size=size) + return DolmosBitVec(0, size=size) - return HalmosBitVec(self._value << shift_amount, size=size) + return DolmosBitVec(self._value << shift_amount, size=size) - def lshr(self, shift: BV) -> "HalmosBitVec": + def lshr(self, shift: BV) -> "DolmosBitVec": """ Logical right shift """ @@ -825,14 +825,14 @@ def lshr(self, shift: BV) -> "HalmosBitVec": return self if self.is_concrete: - return HalmosBitVec(self.value >> shift_amount, size=size) + return DolmosBitVec(self.value >> shift_amount, size=size) if shift_amount >= size: - return HalmosBitVec(0, size=size) + return DolmosBitVec(0, size=size) - return HalmosBitVec(LShR(self.as_z3(), shift.as_z3()), size=size) + return DolmosBitVec(LShR(self.as_z3(), shift.as_z3()), size=size) - def ashr(self, shift: BV) -> "HalmosBitVec": + def ashr(self, shift: BV) -> "DolmosBitVec": """ Arithmetic right shift """ @@ -841,9 +841,9 @@ def ashr(self, shift: BV) -> "HalmosBitVec": if shift.is_concrete and shift.value == 0: return self - return HalmosBitVec(self.as_z3() >> shift.value, size=self.size) + return DolmosBitVec(self.as_z3() >> shift.value, size=self.size) - def clz(self) -> "HalmosBitVec": + def clz(self) -> "DolmosBitVec": """ Count Leading Zeros — EVM opcode CLZ (0x1E, EIP-7939 / Fusaka). @@ -865,7 +865,7 @@ def clz(self) -> "HalmosBitVec": # --- concrete fast path --- if self.is_concrete: v = self._value - return HalmosBitVec(size if v == 0 else size - v.bit_length(), size=size) + return DolmosBitVec(size if v == 0 else size - v.bit_length(), size=size) # --- symbolic path: binary search over bit-levels --- x = self.as_z3() @@ -886,81 +886,81 @@ def clz(self) -> "HalmosBitVec": # x == 0 -> every bit is a leading zero, result is `size`. result = If(x == zero, BitVecVal(size, size), acc) - return HalmosBitVec(result, size=size) + return DolmosBitVec(result, size=size) def bitwise_not(self) -> BV: if self.is_concrete: - return HalmosBitVec(~self._value & ((1 << self._size) - 1), size=self._size) + return DolmosBitVec(~self._value & ((1 << self._size) - 1), size=self._size) - return HalmosBitVec(~self.as_z3(), size=self.size) + return DolmosBitVec(~self.as_z3(), size=self.size) def bitwise_and(self, other: BV) -> BV: assert self._size == other._size - return HalmosBitVec(self._value & other._value, size=self._size) + return DolmosBitVec(self._value & other._value, size=self._size) def bitwise_or(self, other: BV) -> BV: assert self._size == other._size - return HalmosBitVec(self._value | other._value, size=self._size) + return DolmosBitVec(self._value | other._value, size=self._size) def bitwise_xor(self, other: BV) -> BV: assert self._size == other._size - return HalmosBitVec(self._value ^ other._value, size=self._size) + return DolmosBitVec(self._value ^ other._value, size=self._size) - def ult(self, other: BV) -> HalmosBool: + def ult(self, other: BV) -> DolmosBool: assert self._size == other._size if self.is_concrete and other.is_concrete: - return HalmosBool(self.value < other.value) + return DolmosBool(self.value < other.value) - return HalmosBool(ULT(self.as_z3(), other.as_z3())) + return DolmosBool(ULT(self.as_z3(), other.as_z3())) - def ugt(self, other: BV) -> HalmosBool: + def ugt(self, other: BV) -> DolmosBool: assert self._size == other._size if not self._symbolic and not other._symbolic: return TRUE if self.value > other.value else FALSE - return HalmosBool(UGT(self.as_z3(), other.as_z3())) + return DolmosBool(UGT(self.as_z3(), other.as_z3())) - def slt(self, other: BV) -> HalmosBool: + def slt(self, other: BV) -> DolmosBool: assert self._size == other._size if self.is_concrete and other.is_concrete: left = to_signed(self._value, self._size) right = to_signed(other._value, other._size) - return HalmosBool(left < right) + return DolmosBool(left < right) - return HalmosBool(self.as_z3() < other.as_z3()) + return DolmosBool(self.as_z3() < other.as_z3()) - def sgt(self, other: BV) -> HalmosBool: + def sgt(self, other: BV) -> DolmosBool: assert self._size == other._size if self.is_concrete and other.is_concrete: left = to_signed(self._value, self._size) right = to_signed(other._value, other._size) - return HalmosBool(left > right) + return DolmosBool(left > right) - return HalmosBool(self.as_z3() > other.as_z3()) + return DolmosBool(self.as_z3() > other.as_z3()) - def ule(self, other: BV) -> HalmosBool: + def ule(self, other: BV) -> DolmosBool: assert self._size == other._size if self.is_concrete and other.is_concrete: - return HalmosBool(self.value <= other.value) + return DolmosBool(self.value <= other.value) - return HalmosBool(ULE(self.as_z3(), other.as_z3())) + return DolmosBool(ULE(self.as_z3(), other.as_z3())) - def uge(self, other: BV) -> HalmosBool: + def uge(self, other: BV) -> DolmosBool: assert self._size == other._size if self.is_concrete and other.is_concrete: - return HalmosBool(self.value >= other.value) + return DolmosBool(self.value >= other.value) - return HalmosBool(UGE(self.as_z3(), other.as_z3())) + return DolmosBool(UGE(self.as_z3(), other.as_z3())) - def eq(self, other: BV) -> HalmosBool: + def eq(self, other: BV) -> DolmosBool: assert self._size == other._size - return HalmosBool(self._value == other._value) + return DolmosBool(self._value == other._value) def byte(self, idx: int, *, output_size: int = 8) -> BV: """ @@ -982,28 +982,28 @@ def byte(self, idx: int, *, output_size: int = 8) -> BV: assert idx >= 0 if idx >= byte_length: - return HalmosBitVec(0, size=output_size) + return DolmosBitVec(0, size=output_size) if self.is_concrete: b = self._value.to_bytes(length=byte_length, byteorder="big") - return HalmosBitVec(b[idx], size=output_size) + return DolmosBitVec(b[idx], size=output_size) # symbolic case lo = (byte_length - 1 - idx) * 8 hi = lo + 7 - return HalmosBitVec( + return DolmosBitVec( Extract(hi, lo, self._value), size=output_size, ) # initialize global constants -TRUE = object.__new__(HalmosBool) -FALSE = object.__new__(HalmosBool) +TRUE = object.__new__(DolmosBool) +FALSE = object.__new__(DolmosBool) TRUE.con_val = True TRUE.sym_val = None FALSE.con_val = False FALSE.sym_val = None -ZERO = HalmosBitVec(0, size=256) -ONE = HalmosBitVec(1, size=256) +ZERO = DolmosBitVec(0, size=256) +ONE = DolmosBitVec(1, size=256) diff --git a/src/halmos/build.py b/src/dolmos/build.py similarity index 91% rename from src/halmos/build.py rename to src/dolmos/build.py index ad6d5ea0..ed4ec2e0 100644 --- a/src/halmos/build.py +++ b/src/dolmos/build.py @@ -7,11 +7,11 @@ from eth_hash.auto import keccak -from halmos.calldata import str_abi -from halmos.config import Config as HalmosConfig -from halmos.logs import PARSING_ERROR, debug, warn_code -from halmos.mapper import Mapper, SourceFileMap -from halmos.ui import ui +from dolmos.calldata import str_abi +from dolmos.config import Config as DolmosConfig +from dolmos.logs import PARSING_ERROR, debug, warn_code +from dolmos.mapper import Mapper, SourceFileMap +from dolmos.ui import ui SOLIDITY_EXTENSIONS = (".sol",) VYPER_EXTENSIONS = (".vy", ".vyi") @@ -125,13 +125,13 @@ def normalize_vyper_artifact( json_out: dict, dirname: str, root: str, cache_entry ) -> tuple[str, str, dict | None]: """ - Bring a forge vyper artifact into the shape that halmos expects from solc artifacts. + Bring a forge vyper artifact into the shape that dolmos expects from solc artifacts. forge only emits `abi`, `bytecode`, `deployedBytecode`, an empty `methodIdentifiers` and an `id` for vyper, so we fill in: - `methodIdentifiers`, computed from the abi - `bytecode.linkReferences` (vyper has no libraries) - - `metadata.compiler.version` and `metadata.output.devdoc` (for `@custom:halmos`) + - `metadata.compiler.version` and `metadata.output.devdoc` (for `@custom:dolmos`) - `sourcePath` and `language` Returns (contract type, compiler version group key, natspec). @@ -160,7 +160,7 @@ def normalize_vyper_artifact( # vyper modules are always contracts, .vyi files are always interfaces contract_type = "interface" if dirname.endswith(".vyi") else "contract" - # `@custom:halmos` annotations live in docstrings; forge does not give us the + # `@custom:dolmos` annotations live in docstrings; forge does not give us the # devdoc for vyper, so read them straight from the source natspec = None devdoc_methods = {} @@ -173,8 +173,8 @@ def normalize_vyper_artifact( for funsig in json_out["methodIdentifiers"]: doc = function_docs.get(funsig.split("(")[0]) - if doc and (halmos_args := parse_natspec({"text": doc})): - devdoc_methods[funsig] = {"custom:halmos": halmos_args} + if doc and (options := parse_natspec({"text": doc})): + devdoc_methods[funsig] = {"custom:dolmos": options} except OSError as err: debug(f"could not read vyper source {source_path}: {err}") @@ -200,7 +200,7 @@ def get_contract_type( return None, None -def parse_build_out(args: HalmosConfig) -> dict: +def parse_build_out(args: DolmosConfig) -> dict: result = {} # compiler version -> source filename -> contract name -> (json, type) coverage_output = args.coverage_output @@ -325,7 +325,7 @@ def add_to_build_out( contract_map[contract_name] = entry -def parse_symbols(args: HalmosConfig, contract_map: dict, contract_name: str) -> None: +def parse_symbols(args: DolmosConfig, contract_map: dict, contract_name: str) -> None: try: json_out = contract_map[contract_name][0] @@ -347,39 +347,51 @@ def parse_symbols(args: HalmosConfig, contract_map: dict, contract_name: str) -> pass +# natspec tags carrying command line options; @custom:halmos is still accepted so +# that test suites written for halmos keep working unchanged +NATSPEC_TAGS = ("dolmos", "halmos") + + def parse_devdoc(funsig: str, contract_json: dict) -> str | None: try: - return contract_json["metadata"]["output"]["devdoc"]["methods"][funsig][ - "custom:halmos" - ] + method_doc = contract_json["metadata"]["output"]["devdoc"]["methods"][funsig] except KeyError: return None + values = [ + method_doc[f"custom:{tag}"] + for tag in NATSPEC_TAGS + if f"custom:{tag}" in method_doc + ] + return " ".join(values) if values else None + def parse_natspec(natspec: dict) -> str: # This parsing scheme is designed to handle: # # - multiline tags: - # /// @custom:halmos --x + # /// @custom:dolmos --x # /// --y # # - multiple tags: - # /// @custom:halmos --x - # /// @custom:halmos --y + # /// @custom:dolmos --x + # /// @custom:dolmos --y # # - tags that start in the middle of line: - # /// blah blah @custom:halmos --x + # /// blah blah @custom:dolmos --x # /// --y # # In all the above examples, this scheme returns "--x (whitespaces) --y" - isHalmosTag = False + # (@custom:halmos is accepted as well, see NATSPEC_TAGS) + tags = {f"@custom:{tag}" for tag in NATSPEC_TAGS} + is_options_tag = False result = "" for item in re.split(r"(@\S+)", natspec.get("text", "")): - if item == "@custom:halmos": - isHalmosTag = True + if item in tags: + is_options_tag = True elif re.match(r"^@\S", item): - isHalmosTag = False - elif isHalmosTag: + is_options_tag = False + elif is_options_tag: result += item return result.strip() diff --git a/src/halmos/bytevec.py b/src/dolmos/bytevec.py similarity index 99% rename from src/halmos/bytevec.py rename to src/dolmos/bytevec.py index a84d069a..258de25f 100644 --- a/src/halmos/bytevec.py +++ b/src/dolmos/bytevec.py @@ -7,9 +7,9 @@ from sortedcontainers import SortedDict from z3 import BitVecRef, If, eq, is_bool, is_bv, is_bv_value, simplify, substitute -from halmos.bitvec import HalmosBitVec as BV -from halmos.logs import warn -from halmos.utils import ( +from dolmos.bitvec import DolmosBitVec as BV +from dolmos.logs import warn +from dolmos.utils import ( Byte, Word, bv_value_to_bytes, diff --git a/src/halmos/calldata.py b/src/dolmos/calldata.py similarity index 97% rename from src/halmos/calldata.py rename to src/dolmos/calldata.py index 8d35cd4c..08f37fb4 100644 --- a/src/halmos/calldata.py +++ b/src/dolmos/calldata.py @@ -10,10 +10,10 @@ BitVecRef, ) -from halmos.bytevec import ByteVec -from halmos.config import Config as HalmosConfig -from halmos.logs import debug_once -from halmos.utils import con, uid +from dolmos.bytevec import ByteVec +from dolmos.config import Config as DolmosConfig +from dolmos.logs import debug_once +from dolmos.utils import con, uid @dataclass(frozen=True) @@ -108,7 +108,7 @@ class Calldata: # For dynamic parameters not explicitly listed in --array-lengths, default sizes are used: # - For dynamic arrays: the size ranges from 0 to the value of --loop (inclusive). # - For bytes or strings: the size candidates are given by --default-bytes-lengths. - args: HalmosConfig + args: DolmosConfig # `dyn_params` will be updated to include the fully resolved size information for all dynamic parameters. dyn_params: list[DynamicParam] @@ -119,7 +119,7 @@ class Calldata: def __init__( self, - args: HalmosConfig, + args: DolmosConfig, new_symbol_id: Callable | None, ) -> None: self.args = args @@ -329,7 +329,7 @@ def get_abi(contract_json: dict) -> dict: def mk_calldata( abi: dict, fun_info: FunctionInfo, - args: HalmosConfig, + args: DolmosConfig, new_symbol_id: Callable = None, ) -> tuple[ByteVec, list[DynamicParam]]: return Calldata(args, new_symbol_id).create(abi, fun_info) diff --git a/src/halmos/cheatcodes.py b/src/dolmos/cheatcodes.py similarity index 96% rename from src/halmos/cheatcodes.py rename to src/dolmos/cheatcodes.py index 87d4a565..f4ed7bcc 100644 --- a/src/halmos/cheatcodes.py +++ b/src/dolmos/cheatcodes.py @@ -26,27 +26,27 @@ unsat, ) -from halmos import env -from halmos.assertions import assert_cheatcode_handler -from halmos.bitvec import HalmosBitVec as BV -from halmos.bitvec import HalmosBool as Bool -from halmos.bytevec import ByteVec -from halmos.calldata import ( +from dolmos import env +from dolmos.assertions import assert_cheatcode_handler +from dolmos.bitvec import DolmosBitVec as BV +from dolmos.bitvec import DolmosBool as Bool +from dolmos.bytevec import ByteVec +from dolmos.calldata import ( FunctionInfo, get_abi, mk_calldata, ) -from halmos.constants import MAX_MEMORY_SIZE -from halmos.exceptions import ( +from dolmos.constants import MAX_MEMORY_SIZE +from dolmos.exceptions import ( + DolmosException, FailCheatcode, - HalmosException, InfeasiblePath, NotConcreteError, OutOfGasError, ) -from halmos.logs import debug -from halmos.mapper import BuildOut -from halmos.utils import ( +from dolmos.logs import debug +from dolmos.mapper import BuildOut +from dolmos.utils import ( Address, BitVecSort8, BitVecSort160, @@ -195,7 +195,7 @@ def lookup(self, to: BV) -> PrankResult: """ assert_address(to) - if self and to not in [halmos_cheat_code.address, hevm_cheat_code.address]: + if self and to not in [svm_cheat_code.address, hevm_cheat_code.address]: result = self.active if not self.keep: self.stopPrank() @@ -232,7 +232,7 @@ def symbolic_storage(ex, arg, sevm, stack): if account_alias is None: error_msg = f"enableSymbolicStorage() or setArbitraryStorage() is not allowed for a nonexistent account: {hexify(account)}" - raise HalmosException(error_msg) + raise DolmosException(error_msg) ex.storage[account_alias].symbolic = True @@ -247,7 +247,7 @@ def snapshot_storage(ex, arg, sevm, stack): if account_alias is None: error_msg = f"snapshotStorage() is not allowed for a nonexistent account: {hexify(account)}" - raise HalmosException(error_msg) + raise DolmosException(error_msg) zero_pad = b"\x00" * 16 return ByteVec(zero_pad + ex.storage[account_alias].digest()) @@ -300,10 +300,10 @@ def _get_contract_name(ex, arg) -> tuple[str | None, str | None]: ) if not (code := ex.code.get(addr)): - raise HalmosException(f"createCalldata: not contract account: {hexify(addr)}") + raise DolmosException(f"createCalldata: not contract account: {hexify(addr)}") if not (contract_name := code.contract_name): - raise HalmosException( + raise DolmosException( f"createCalldata: couldn't find the contract name for: {hexify(addr)}" ) @@ -437,14 +437,14 @@ def create_generic(ex, bits: int, var_name: str, type_name: str) -> BitVecRef | if not bits: return ByteVec() - label = f"halmos_{var_name}_{type_name}_{uid()}_{ex.new_symbol_id():>02}" + label = f"dolmos_{var_name}_{type_name}_{uid()}_{ex.new_symbol_id():>02}" return BitVec(label, BitVecSorts[bits]) def create_uint(ex, arg, name: str | None = None, **kwargs): bits = int_of(arg.get_word(4), "symbolic bit size for svm.createUint()") if bits > 256: - raise HalmosException(f"bitsize larger than 256: {bits}") + raise DolmosException(f"bitsize larger than 256: {bits}") name = name or name_of(extract_string_argument(arg, 1)) return ByteVec(uint256(create_generic(ex, bits, name, f"uint{bits}"))) @@ -458,7 +458,7 @@ def create_uint256(ex, arg, name: str | None = None, **kwargs): def create_int(ex, arg, name: str | None = None, **kwargs): bits = int_of(extract_word(arg, 4), "symbolic bit size for svm.createUint()") if bits > 256: - raise HalmosException(f"bitsize larger than 256: {bits}") + raise DolmosException(f"bitsize larger than 256: {bits}") name = name or name_of(extract_string_argument(arg, 1)) return ByteVec(int256(create_generic(ex, bits, name, f"int{bits}"))) @@ -522,7 +522,7 @@ def create_uint256_min_max(ex, arg, name: str | None = None, **kwargs): symbolic_value = create_generic(ex, 256, name, "uint256") if min_value > max_value: - raise HalmosException(f"expected min={min_value} <= max={max_value}") + raise DolmosException(f"expected min={min_value} <= max={max_value}") # Add constraints for the symbolic value to be within the specified range min_condition = simplify(UGE(symbolic_value, min_value)) @@ -876,7 +876,7 @@ def apply_vmaddr(ex, private_key: Word): return addr -class halmos_cheat_code: +class svm_cheat_code: # address constant SVM_ADDRESS = # address(bytes20(uint160(uint256(keccak256('svm cheat code'))))); address = BV(0xF3993A62377BCD56AE39D773740A5390411E8BC9, size=160) @@ -906,13 +906,13 @@ class halmos_cheat_code: @staticmethod def handle(sevm, ex, arg: ByteVec, stack) -> list[BitVecRef]: - funsig = int_of(extract_funsig(arg), "symbolic halmos cheatcode") - if handler := halmos_cheat_code.handlers.get(funsig): + funsig = int_of(extract_funsig(arg), "symbolic svm cheatcode") + if handler := svm_cheat_code.handlers.get(funsig): result = handler(ex, arg, sevm=sevm, stack=stack) return result if isinstance(result, list) else [result] - error_msg = f"Unknown halmos cheat code: function selector = 0x{funsig:0>8x}, calldata = {hexify(arg)}" - raise HalmosException(error_msg) + error_msg = f"Unknown svm cheat code: function selector = 0x{funsig:0>8x}, calldata = {hexify(arg)}" + raise DolmosException(error_msg) class hevm_cheat_code: @@ -1180,7 +1180,7 @@ def handle(sevm, ex, arg: ByteVec, stack) -> ByteVec | None: sender = uint160(arg.get_word(4)).as_z3() result = ex.context.prank.prank(sender) if not result: - raise HalmosException( + raise DolmosException( "can not call vm.prank(address) with an active prank" ) return ret @@ -1191,7 +1191,7 @@ def handle(sevm, ex, arg: ByteVec, stack) -> ByteVec | None: origin = uint160(arg.get_word(36)).as_z3() result = ex.context.prank.prank(sender, origin) if not result: - raise HalmosException( + raise DolmosException( "can not call vm.prank(address, address) with an active prank" ) return ret @@ -1201,7 +1201,7 @@ def handle(sevm, ex, arg: ByteVec, stack) -> ByteVec | None: address = uint160(arg.get_word(4)).as_z3() result = ex.context.prank.startPrank(address) if not result: - raise HalmosException( + raise DolmosException( "can not call vm.startPrank(address) with an active prank" ) return ret @@ -1212,7 +1212,7 @@ def handle(sevm, ex, arg: ByteVec, stack) -> ByteVec | None: origin = uint160(arg.get_word(36)).as_z3() result = ex.context.prank.startPrank(sender, origin) if not result: - raise HalmosException( + raise DolmosException( "can not call vm.startPrank(address, address) with an active prank" ) return ret @@ -1249,11 +1249,11 @@ def handle(sevm, ex, arg: ByteVec, stack) -> ByteVec | None: who = uint160(arg.get_word(4)) if not who.is_concrete: error_msg = f"vm.{name}(address who, uint64 nonce) must have concrete argument `who` but received {who}" - raise HalmosException(error_msg) + raise DolmosException(error_msg) nonce = int_of(arg.get_word(36), f"symbolic nonce in vm.{name}()") if not (0 <= nonce < 2**64): - raise HalmosException( + raise DolmosException( f"vm.{name}(): nonce out of uint64 range: {nonce}" ) @@ -1267,7 +1267,7 @@ def handle(sevm, ex, arg: ByteVec, stack) -> ByteVec | None: f"the current nonce ({current}) of {hexify(who)}; " f"use vm.setNonceUnsafe() to decrease it" ) - raise HalmosException(error_msg) + raise DolmosException(error_msg) ex.set_nonce(who, nonce) return ret @@ -1277,7 +1277,7 @@ def handle(sevm, ex, arg: ByteVec, stack) -> ByteVec | None: who = uint160(arg.get_word(4)) if not who.is_concrete: error_msg = f"vm.resetNonce(address who) must have concrete argument `who` but received {who}" - raise HalmosException(error_msg) + raise DolmosException(error_msg) who = who.as_z3() @@ -1302,7 +1302,7 @@ def handle(sevm, ex, arg: ByteVec, stack) -> ByteVec | None: if store_account_alias is None: error_msg = f"vm.store() is not allowed for a nonexistent account: {hexify(store_account)}" - raise HalmosException(error_msg) + raise DolmosException(error_msg) store_slot = uint256(arg.get_word(36)) store_value = uint256(arg.get_word(68)) @@ -1362,7 +1362,7 @@ def handle(sevm, ex, arg: ByteVec, stack) -> ByteVec | None: if not who.is_concrete: error_msg = f"vm.etch(address who, bytes code) must have concrete argument `who` but received {who}" - raise HalmosException(error_msg) + raise DolmosException(error_msg) # code must be concrete code_offset = int_of(arg.get_word(36), "symbolic code offset") @@ -1388,7 +1388,7 @@ def handle(sevm, ex, arg: ByteVec, stack) -> ByteVec | None: elif funsig == hevm_cheat_code.ffi_sig: if not sevm.options.ffi: error_msg = "ffi cheatcode is disabled. Run again with `--ffi` if you want to enable it" - raise HalmosException(error_msg) + raise DolmosException(error_msg) cmd = extract_string_array_argument(arg, 0) @@ -1621,9 +1621,9 @@ def handle(sevm, ex, arg: ByteVec, stack) -> ByteVec | None: elif funsig in dict_of_unsupported_cheatcodes: msg = f"Unsupported cheat code: {dict_of_unsupported_cheatcodes[funsig]}" - raise HalmosException(msg) + raise DolmosException(msg) else: # TODO: support other cheat codes msg = f"Unsupported cheat code: calldata = {hexify(arg)}" - raise HalmosException(msg) + raise DolmosException(msg) diff --git a/src/halmos/config.py b/src/dolmos/config.py similarity index 96% rename from src/halmos/config.py rename to src/dolmos/config.py index 0ed9c010..6bb84cd1 100644 --- a/src/halmos/config.py +++ b/src/dolmos/config.py @@ -15,9 +15,9 @@ import toml -from halmos.logs import warn -from halmos.solvers import SOLVERS, get_solver_command -from halmos.utils import parse_time +from dolmos.logs import warn +from dolmos.solvers import SOLVERS, get_solver_command +from dolmos.utils import parse_time # common strings internal = "internal" @@ -45,13 +45,13 @@ class ConfigSource(IntEnum): # default value, lowest precedence default = 1 - # e.g. halmos.toml + # e.g. dolmos.toml config_file = 2 - # contract-level annotation (e.g. @custom:halmos --some-option) + # contract-level annotation (e.g. @custom:dolmos --some-option) contract_annotation = 3 - # function-level annotation (e.g. @custom:halmos --some-option) + # function-level annotation (e.g. @custom:dolmos --some-option) function_annotation = 4 # from command line, highest precedence @@ -213,7 +213,7 @@ def unparse(values: dict[str, list[int]]) -> str: # TODO: add kw_only=True when we support Python>=3.10 @dataclass(frozen=True) class Config: - """Configuration object for halmos. + """Configuration object for dolmos. Don't instantiate this directly, since all fields have default value None. Instead, use: @@ -261,8 +261,8 @@ class Config: config: str = arg( help="path to the config file", metavar="FILE", - global_default=lambda: os.path.join(os.getcwd(), "halmos.toml"), - global_default_str="ROOT/halmos.toml", + global_default=lambda: os.path.join(os.getcwd(), "dolmos.toml"), + global_default_str="ROOT/dolmos.toml", ) contract: str = arg( @@ -860,9 +860,9 @@ def _create_default_config() -> "Config": def _create_arg_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( - # use the name the command was invoked with (halmos, dolmos, ...) - prog=os.path.basename(sys.argv[0]) or "halmos", - epilog="For more information, see https://github.com/a16z/halmos", + # use the name the command was invoked with + prog=program_name(), + epilog="For more information, see https://github.com/Dowsers/dolmos", ) groups = { @@ -927,6 +927,12 @@ def default_config() -> "Config": return _default_config +def program_name() -> str: + """The name the command was invoked with (`dolmos` for `python -m dolmos`).""" + name = os.path.basename(sys.argv[0]) + return "dolmos" if not name or name.endswith(".py") else name + + def arg_parser() -> argparse.ArgumentParser: return _arg_parser @@ -942,7 +948,7 @@ def toml_parser(): # can generate a sample config file using: -# python -m halmos.config ARGS > halmos.toml +# python -m dolmos.config ARGS > dolmos.toml def main(): def _to_toml_str(value: Any, type) -> str: assert value is not None @@ -957,16 +963,7 @@ def _to_toml_str(value: Any, type) -> str: source=ConfigSource.command_line, **vars(args) ) - # devs can have a little easter egg - lines = [ - "# ___ ___ ___ ___ ___ ___", - "# /\\__\\ /\\ \\ /\\__\\ /\\__\\ /\\ \\ /\\ \\", - "# /:/__/_ /::\\ \\ /:/ / /::L_L_ /::\\ \\ /::\\ \\", - "# /::\\/\\__\\ /::\\:\\__\\ /:/__/ /:/L:\\__\\ /:/\\:\\__\\ /\\:\\:\\__\\", - "# \\/\\::/ / \\/\\::/ / \\:\\ \\ \\/_/:/ / \\:\\/:/ / \\:\\:\\/__/", - "# /:/ / /:/ / \\:\\__\\ /:/ / \\::/ / \\::/ /", - "# \\/__/ \\/__/ \\/__/ \\/__/ \\/__/ \\/__/", - ] + lines = ["# dolmos configuration, generated by `python -m dolmos.config`"] lines.append("\n[global]") current_group_name = None diff --git a/src/halmos/console.py b/src/dolmos/console.py similarity index 97% rename from src/halmos/console.py rename to src/dolmos/console.py index 9a219172..56a2269f 100644 --- a/src/halmos/console.py +++ b/src/dolmos/console.py @@ -2,12 +2,12 @@ from z3 import BitVec -from halmos.bitvec import HalmosBitVec as BV -from halmos.logs import ( +from dolmos.bitvec import DolmosBitVec as BV +from dolmos.logs import ( info, warn, ) -from halmos.utils import ( +from dolmos.utils import ( extract_bytes, extract_bytes_argument, extract_funsig, diff --git a/src/halmos/constants.py b/src/dolmos/constants.py similarity index 83% rename from src/halmos/constants.py rename to src/dolmos/constants.py index 7511cad3..ae5296f5 100644 --- a/src/halmos/constants.py +++ b/src/dolmos/constants.py @@ -8,5 +8,5 @@ MAX_MEMORY_SIZE = 2**20 # to be compatible with startHoax(address), -# see https://github.com/a16z/halmos/issues/338 +# see upstream halmos issue #338 MAX_ETH = 1 << 128 diff --git a/src/halmos/contract.py b/src/dolmos/contract.py similarity index 98% rename from src/halmos/contract.py rename to src/dolmos/contract.py index d1b8a268..20d798e3 100644 --- a/src/halmos/contract.py +++ b/src/dolmos/contract.py @@ -4,19 +4,19 @@ from dataclasses import dataclass from typing import ClassVar -from halmos.bitvec import HalmosBitVec as BV -from halmos.bytevec import ByteVec, ConcreteChunk -from halmos.constants import MAX_MEMORY_SIZE -from halmos.exceptions import ( +from dolmos.bitvec import DolmosBitVec as BV +from dolmos.bytevec import ByteVec, ConcreteChunk +from dolmos.constants import MAX_MEMORY_SIZE +from dolmos.exceptions import ( NotConcreteError, OutOfGasError, ) -from halmos.logs import ( +from dolmos.logs import ( LIBRARY_PLACEHOLDER, warn_code, ) -from halmos.mapper import SingletonMeta, SourceFileMap -from halmos.utils import ( +from dolmos.mapper import SingletonMeta, SourceFileMap +from dolmos.utils import ( Address, Byte, con_addr, diff --git a/src/halmos/env.py b/src/dolmos/env.py similarity index 98% rename from src/halmos/env.py rename to src/dolmos/env.py index bcd4386b..14dad12d 100644 --- a/src/halmos/env.py +++ b/src/dolmos/env.py @@ -6,8 +6,8 @@ from dotenv import find_dotenv, load_dotenv -from halmos.logs import debug -from halmos.utils import Address, uint160 +from dolmos.logs import debug +from dolmos.utils import Address, uint160 def init_env(path: str | None = None): diff --git a/src/halmos/exceptions.py b/src/dolmos/exceptions.py similarity index 97% rename from src/halmos/exceptions.py rename to src/dolmos/exceptions.py index 300d77f6..74b64e57 100644 --- a/src/halmos/exceptions.py +++ b/src/dolmos/exceptions.py @@ -20,7 +20,7 @@ class PathEndingException(Exception): pass -class HalmosException(PathEndingException): +class DolmosException(PathEndingException): """ Base class for unexpected internal errors happening during a test run. Inherits from RunEndingException because it should stop further path exploration. @@ -29,7 +29,7 @@ class HalmosException(PathEndingException): pass -class NotConcreteError(HalmosException): +class NotConcreteError(DolmosException): pass diff --git a/src/halmos/flamegraphs.py b/src/dolmos/flamegraphs.py similarity index 97% rename from src/halmos/flamegraphs.py rename to src/dolmos/flamegraphs.py index 4502ea51..3bd923f4 100644 --- a/src/halmos/flamegraphs.py +++ b/src/dolmos/flamegraphs.py @@ -7,10 +7,10 @@ import traceback from dataclasses import dataclass, field -from halmos.logs import debug, warn -from halmos.sevm import CallContext, CallSequence, Message -from halmos.traces import rendered_address -from halmos.utils import hexify +from dolmos.logs import debug, warn +from dolmos.sevm import CallContext, CallSequence, Message +from dolmos.traces import rendered_address +from dolmos.utils import hexify AUTO_REFRESH_INTERVAL_SECONDS = 0.250 diff --git a/src/halmos/hashes.py b/src/dolmos/hashes.py similarity index 100% rename from src/halmos/hashes.py rename to src/dolmos/hashes.py diff --git a/src/halmos/logs.py b/src/dolmos/logs.py similarity index 91% rename from src/halmos/logs.py rename to src/dolmos/logs.py index f44a2b18..05d7814c 100644 --- a/src/halmos/logs.py +++ b/src/dolmos/logs.py @@ -14,7 +14,7 @@ handlers=[RichHandler(level=logging.NOTSET, show_time=False, show_path=False)], ) -logger = logging.getLogger("halmos") +logger = logging.getLogger("dolmos") # @@ -33,7 +33,7 @@ def filter(self, record): return True -logger_unique = logging.getLogger("halmos.unique") +logger_unique = logging.getLogger("dolmos.unique") logger_unique.addFilter(UniqueLoggingFilter()) @@ -65,7 +65,7 @@ def debug_once(text: str) -> None: # Warnings with error code # -WARNINGS_BASE_URL = "https://github.com/a16z/halmos/wiki/warnings" +WARNINGS_BASE_URL = "https://github.com/Dowsers/dolmos/blob/main/docs/warnings.md" @dataclass diff --git a/src/halmos/mapper.py b/src/dolmos/mapper.py similarity index 98% rename from src/halmos/mapper.py rename to src/dolmos/mapper.py index 562c6aec..5425efc3 100644 --- a/src/halmos/mapper.py +++ b/src/dolmos/mapper.py @@ -6,8 +6,8 @@ from dataclasses import dataclass, field from typing import Any, ForwardRef, Optional -from halmos.exceptions import HalmosException -from halmos.logs import debug +from dolmos.exceptions import DolmosException +from dolmos.logs import debug SELECTOR_FIELDS = { "VariableDeclaration": "functionSelector", @@ -345,7 +345,7 @@ def get_by_name(self, contract_name: str, filename: str = None) -> dict: """ Return the build output json for the given contract name. - Raise a HalmosException if the contract is not found, or cannot be uniquely determined. + Raise a DolmosException if the contract is not found, or cannot be uniquely determined. The optional filename argument is required, if the contract name appears in multiple files. """ @@ -355,11 +355,11 @@ def get_by_name(self, contract_name: str, filename: str = None) -> dict: mapping = self._build_out_map_reverse[contract_name] if not mapping: - raise HalmosException(f"{contract_name} is not found") + raise DolmosException(f"{contract_name} is not found") if filename is None: if len(mapping) > 1: - raise HalmosException( + raise DolmosException( f"{contract_name} exists in multiple files: {list(mapping.keys())}" ) [filename] = mapping.keys() @@ -367,7 +367,7 @@ def get_by_name(self, contract_name: str, filename: str = None) -> dict: result = mapping.get(filename) if not result: - raise HalmosException(f"{contract_name} is not found in {filename}") + raise DolmosException(f"{contract_name} is not found in {filename}") return result @@ -513,7 +513,7 @@ def main(): import json import sys - from halmos.utils import cyan + from dolmos.utils import cyan def read_json_file(file_path: str) -> dict: with open(file_path) as f: diff --git a/src/halmos/memtrace.py b/src/dolmos/memtrace.py similarity index 99% rename from src/halmos/memtrace.py rename to src/dolmos/memtrace.py index e82db5a9..737bf2ca 100644 --- a/src/halmos/memtrace.py +++ b/src/dolmos/memtrace.py @@ -8,7 +8,7 @@ from rich.console import Console -from halmos.logs import debug +from dolmos.logs import debug console = Console() diff --git a/src/halmos/processes.py b/src/dolmos/processes.py similarity index 98% rename from src/halmos/processes.py rename to src/dolmos/processes.py index 37782756..30764e23 100644 --- a/src/halmos/processes.py +++ b/src/dolmos/processes.py @@ -80,7 +80,7 @@ def run(): self.set_result((self.stdout, self.stderr, self.returncode)) # avoid daemon threads because they can cause issues during shutdown - # we don't expect them to actually prevent halmos from terminating, + # we don't expect them to actually prevent dolmos from terminating, # as long as the underlying processes are terminated (either by natural # causes or by forceful termination) threading.Thread(target=run, daemon=False).start() @@ -117,7 +117,7 @@ def cancel(self): # ensure file descriptors are closed after process termination. # note: when a process is killed via psutil, the file descriptors remain open on the Python side, - # leading to resource leaks. see: https://github.com/a16z/halmos/issues/523 + # leading to resource leaks. see upstream halmos issue #523 if self.process: for stream in [ self.process.stdout, diff --git a/src/halmos/sevm.py b/src/dolmos/sevm.py similarity index 98% rename from src/halmos/sevm.py rename to src/dolmos/sevm.py index 32e6b1c6..fddebdfe 100644 --- a/src/halmos/sevm.py +++ b/src/dolmos/sevm.py @@ -54,16 +54,16 @@ ) from z3.z3util import is_expr_var -from halmos.bitvec import ONE, ZERO, is_power_of_two -from halmos.bitvec import HalmosBitVec as BV -from halmos.bitvec import HalmosBool as Bool -from halmos.bytevec import ByteVec, SymbolicChunk -from halmos.calldata import FunctionInfo -from halmos.cheatcodes import Prank, halmos_cheat_code, hevm_cheat_code -from halmos.config import Config as HalmosConfig -from halmos.console import console -from halmos.constants import MAX_ETH, MAX_MEMORY_SIZE -from halmos.contract import ( +from dolmos.bitvec import ONE, ZERO, is_power_of_two +from dolmos.bitvec import DolmosBitVec as BV +from dolmos.bitvec import DolmosBool as Bool +from dolmos.bytevec import ByteVec, SymbolicChunk +from dolmos.calldata import FunctionInfo +from dolmos.cheatcodes import Prank, hevm_cheat_code, svm_cheat_code +from dolmos.config import Config as DolmosConfig +from dolmos.console import console +from dolmos.constants import MAX_ETH, MAX_MEMORY_SIZE +from dolmos.contract import ( CALL_OPCODES, CREATE_OPCODES, OP_ADD, @@ -160,11 +160,11 @@ Instruction, mnemonic, ) -from halmos.exceptions import ( +from dolmos.exceptions import ( AddressCollision, + DolmosException, EvmException, FailCheatcode, - HalmosException, InfeasiblePath, InsufficientFunds, InvalidJumpDestError, @@ -178,16 +178,16 @@ StackUnderflowError, WriteInStaticContext, ) -from halmos.logs import ( +from dolmos.logs import ( INTERNAL_ERROR, debug, debug_once, warn, warn_code, ) -from halmos.mapper import BuildOut -from halmos.ui import ui -from halmos.utils import ( +from dolmos.mapper import BuildOut +from dolmos.ui import ui +from dolmos.utils import ( Address, BitVecSort160, BitVecSort256, @@ -263,7 +263,7 @@ CHEATCODE_ADDRESSES: tuple[BV, ...] = ( hevm_cheat_code.address, - halmos_cheat_code.address, + svm_cheat_code.address, console.address, ) @@ -522,7 +522,7 @@ class CallOutput: data: ByteVec | None = None accounts_to_delete: set[Address] = field(default_factory=set) - error: EvmException | HalmosException | None = None + error: EvmException | DolmosException | None = None return_scheme: int | None = None # TODO: @@ -607,13 +607,13 @@ def is_stuck(self) -> bool: This is meaningless during execution, because the call may not yet have an output """ data, error = self.output.data, self.output.error - return data is None or isinstance(error, HalmosException) + return data is None or isinstance(error, DolmosException) - def get_stuck_reason(self) -> HalmosException | None: + def get_stuck_reason(self) -> DolmosException | None: """ Returns the first internal error encountered during the execution of the call. """ - if isinstance(self.output.error, HalmosException): + if isinstance(self.output.error, DolmosException): return self.output.error if self.output.data is not None: @@ -1320,10 +1320,10 @@ def halt( ) -> None: output = self.context.output if output.data is not None: - raise HalmosException("output already set") + raise DolmosException("output already set") if data is not None and not isinstance(data, ByteVec): - raise HalmosException(f"invalid output data {data}") + raise DolmosException(f"invalid output data {data}") output.data = data output.error = error @@ -1508,13 +1508,13 @@ def balance_of(self, addr: Word) -> Word: # practical assumption on the max balance per account if is_bv_value(value): if (v := value.as_long()) > MAX_ETH: - raise HalmosException(f"balance {v} > MAX_ETH") + raise DolmosException(f"balance {v} > MAX_ETH") else: cond = simplify(ULE(value, con(MAX_ETH))) # stop the current path if we know the balance is definitely greater than MAX_ETH if is_false(cond): - raise HalmosException(f"balance {value} > MAX_ETH") + raise DolmosException(f"balance {value} > MAX_ETH") self.path.append(cond) @@ -1599,7 +1599,7 @@ def sha3_data(self, data: Bytes) -> Word: sha3_hash_int = int.from_bytes(sha3_hash, "big") if sha3_hash_int == 0 or sha3_hash_int > 2**256 - 2**64: error_msg = f"hash value outside expected range: {sha3_hash.hex()}" - raise HalmosException(error_msg) + raise DolmosException(error_msg) else: # assume hash values are non-zero and sufficiently small to prevent overflow when adding reasonable offsets @@ -2162,13 +2162,13 @@ def bitwise(op, x: Word, y: Word) -> Word: raise ValueError(op, x, y) -class HalmosLogs: +class DolmosLogs: bounded_loops: list[JumpID] def __init__(self) -> None: self.bounded_loops = [] - def extend(self, logs: "HalmosLogs") -> None: + def extend(self, logs: "DolmosLogs") -> None: self.bounded_loops.extend(logs.bounded_loops) @@ -2193,15 +2193,15 @@ def __len__(self) -> int: class SEVM: - options: HalmosConfig + options: DolmosConfig fun_info: FunctionInfo storage_model: type[SomeStorage] - logs: HalmosLogs + logs: DolmosLogs - def __init__(self, options: HalmosConfig, fun_info: FunctionInfo) -> None: + def __init__(self, options: DolmosConfig, fun_info: FunctionInfo) -> None: self.options = options self.fun_info = fun_info - self.logs = HalmosLogs() + self.logs = DolmosLogs() # init storage model is_generic = self.options.storage_layout == "generic" @@ -2367,7 +2367,7 @@ def resolve_address_alias( head, *tail = potential_aliases if not allow_branching and tail: - raise HalmosException(f"multiple aliases exist: {hexify(target)}") + raise DolmosException(f"multiple aliases exist: {hexify(target)}") for addr, cond in tail: new_ex = self.create_branch(ex, cond, ex.pc) @@ -2683,10 +2683,10 @@ def call_unknown() -> None: ) ret = ByteVec(f_point_evaluation(wrapped)) - # halmos cheat code - elif to == halmos_cheat_code.address: + # svm cheat code + elif to == svm_cheat_code.address: exit_code = ONE - ret = halmos_cheat_code.handle(self, ex, arg, stack) + ret = svm_cheat_code.handle(self, ex, arg, stack) # vm cheat code elif to == hevm_cheat_code.address: @@ -2728,7 +2728,7 @@ def call_unknown() -> None: last_idx = len(ret_lst) - 1 for idx, ret_ in enumerate(ret_lst): if not isinstance(ret_, ByteVec): - raise HalmosException(f"Invalid return value: {ret_}") + raise DolmosException(f"Invalid return value: {ret_}") new_ex = ( self.create_branch(ex, BoolVal(True), ex.pc) @@ -2807,7 +2807,7 @@ def create( ) new_addr = uint160(ex.sha3_data(hash_data)).as_z3() else: - raise HalmosException(f"Unknown CREATE opcode: {op}") + raise DolmosException(f"Unknown CREATE opcode: {op}") # both CREATE and CREATE2 increment the creator's nonce, # regardless of whether the creation succeeds @@ -3564,13 +3564,13 @@ def finalize(ex: Exec): if account_alias is not None: codesize = BV(len(ex.code[account_alias])) else: - # NOTE: the codesize of halmos cheatcode should be non-zero to pass the extcodesize check + # NOTE: the codesize of the svm cheatcode should be non-zero to pass the extcodesize check # for external calls with non-empty return types. this behavior differs from foundry. # the codesize of console is considered zero in foundry codesize = ( ONE # dummy arbitrary value, consistent with foundry if account - in [hevm_cheat_code.address, halmos_cheat_code.address] + in [hevm_cheat_code.address, svm_cheat_code.address] else ZERO ) @@ -3756,7 +3756,7 @@ def finalize(ex: Exec): else: # 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 {mnemonic(opcode)}") + raise DolmosException(f"Unsupported opcode {mnemonic(opcode)}") ex.advance(pc=insn.next_pc) next_ex = ex @@ -3770,7 +3770,7 @@ def finalize(ex: Exec): yield from finalize(ex) continue - except HalmosException as err: + except DolmosException as err: debug(err) ex.halt(data=None, error=err) yield from finalize(ex) diff --git a/src/halmos/solve.py b/src/dolmos/solve.py similarity index 96% rename from src/halmos/solve.py rename to src/dolmos/solve.py index a167811d..d38edfcd 100644 --- a/src/halmos/solve.py +++ b/src/dolmos/solve.py @@ -12,20 +12,20 @@ from z3 import CheckSatResult, Solver, sat, unknown, unsat -from halmos.calldata import FunctionInfo -from halmos.config import Config as HalmosConfig -from halmos.logs import ( +from dolmos.calldata import FunctionInfo +from dolmos.config import Config as DolmosConfig +from dolmos.logs import ( debug, error, warn, ) -from halmos.processes import ( +from dolmos.processes import ( ExecutorRegistry, PopenExecutor, PopenFuture, ) -from halmos.sevm import Address, Exec, SMTQuery -from halmos.utils import hexify +from dolmos.sevm import Address, Exec, SMTQuery +from dolmos.utils import hexify EXIT_TIMEDOUT = 124 @@ -57,11 +57,11 @@ class ModelVariable: ModelVariables = dict[str, ModelVariable] -# Regular expression for capturing halmos variables -halmos_var_pattern = re.compile( +# Regular expression for capturing dolmos variables +dolmos_var_pattern = re.compile( r""" \(\s*define-fun\s+ # Match "(define-fun" - \|?((?:halmos_|p_)[^ |]+)\|?\s+ # Capture either halmos_* or p_*, optionally wrapped in "|" + \|?((?:dolmos_|p_)[^ |]+)\|?\s+ # Capture either dolmos_* or p_*, optionally wrapped in "|" \(\)\s+\(_\s+([^ ]+)\s+ # Capture the SMTLIB type (e.g., "BitVec 256") (\d+)\)\s+ # Capture the bit-width or type argument ( # Group for the value @@ -120,7 +120,7 @@ def empty() -> "InvariantTestingContext": @dataclass(frozen=True, slots=True) class ContractContext: # config with contract-specific overrides - args: HalmosConfig + args: DolmosConfig # name of this contract name: str @@ -178,7 +178,7 @@ class SolvingContext: @dataclass(frozen=True) class FunctionContext: # config with function-specific overrides - args: HalmosConfig + args: DolmosConfig # function name, signature, and selector info: FunctionInfo @@ -278,7 +278,7 @@ def append_unsat_core(self, unsat_core: list[str]) -> None: @dataclass(frozen=True) class PathContext: - args: HalmosConfig + args: DolmosConfig path_id: int solving_ctx: SolvingContext query: SMTQuery @@ -390,7 +390,7 @@ def parse_const_value(value: str) -> int: raise ValueError(f"unknown value format: {value}") -def _parse_halmos_var_match(match: re.Match) -> ModelVariable: +def _parse_dolmos_var_match(match: re.Match) -> ModelVariable: full_name = match.group(1).strip() smt_type = f"{match.group(2)} {match.group(3)}" size_bits = int(match.group(3)) @@ -415,7 +415,7 @@ def parse_model_str(smtlib_str: str) -> ModelVariables: """Expects a whole smtlib model output file, as produced by a solver in response to a `(check-sat)` + `(get-model)` command. - Extracts halmos variables and returns them grouped by their full name""" + Extracts dolmos variables and returns them grouped by their full name""" model_variables: dict[str, ModelVariable] = {} @@ -425,9 +425,9 @@ def parse_model_str(smtlib_str: str) -> ModelVariables: # - ignores the initial sat/unsat on the first line # - ignores the occasional `(model)` command used by yices, stp, cvc4, etc. - for match in halmos_var_pattern.finditer(smtlib_str): + for match in dolmos_var_pattern.finditer(smtlib_str): try: - variable = _parse_halmos_var_match(match) + variable = _parse_dolmos_var_match(match) model_variables[variable.full_name] = variable except Exception as e: error(f"error parsing smtlib string '{match.string.strip()}': {e}") diff --git a/src/halmos/solvers.py b/src/dolmos/solvers.py similarity index 96% rename from src/halmos/solvers.py rename to src/dolmos/solvers.py index 2c9ad468..1287c47f 100644 --- a/src/halmos/solvers.py +++ b/src/dolmos/solvers.py @@ -14,11 +14,11 @@ import requests -from halmos.logs import debug, error -from halmos.ui import ui -from halmos.utils import format_size, synchronized +from dolmos.logs import debug, error +from dolmos.ui import ui +from dolmos.utils import format_size, synchronized -# not defaulting to latest because of https://github.com/a16z/halmos/issues/492 +# not defaulting to latest because of upstream halmos issue #492 DEFAULT_YICES_VERSION = "2.6.4" DEFAULT_CVC5_VERSION = "1.2.1" @@ -26,11 +26,13 @@ DEFAULT_BITWUZLA_VERSION = "0.8.1" # Define the cache directory for solvers -SOLVER_CACHE_DIR = Path.home() / ".halmos" / "solvers" +SOLVER_CACHE_DIR = Path.home() / ".dolmos" / "solvers" SOLVER_CACHE_DIR.mkdir(parents=True, exist_ok=True) # environment variable to bypass the interactive download confirmation prompt -ALLOW_DOWNLOAD_VAR = "HALMOS_ALLOW_DOWNLOAD" +# (HALMOS_ALLOW_DOWNLOAD is still honored for existing setups) +ALLOW_DOWNLOAD_VAR = "DOLMOS_ALLOW_DOWNLOAD" +LEGACY_ALLOW_DOWNLOAD_VAR = "HALMOS_ALLOW_DOWNLOAD" def yices_base_url(version: str) -> str: @@ -349,7 +351,10 @@ def download_allowed(solver: SolverInfo, download_info: DownloadInfo) -> bool: Checks if the download is allowed. """ - bypass = os.environ.get(ALLOW_DOWNLOAD_VAR, "false").lower() in ["true", "1"] + bypass = any( + os.environ.get(var, "false").lower() in ["true", "1"] + for var in (ALLOW_DOWNLOAD_VAR, LEGACY_ALLOW_DOWNLOAD_VAR) + ) if bypass: return True @@ -502,7 +507,7 @@ def get_solver_command(solver_name: str) -> list[str]: if not solver_info: # should have been caught by the config parsing: # `--solver ` is a high level command that expects a supported solver - # solvers not managed by halmos can be accessed with the low-level + # solvers not managed by dolmos can be accessed with the low-level # `--solver-command "solver_binary "` raise ValueError(f"Unsupported solver: {solver_name}") @@ -515,7 +520,7 @@ def get_solver_command(solver_name: str) -> list[str]: return command -# python -m halmos.solvers +# python -m dolmos.solvers # useful when adding a new solver: # - tests the download for all platforms # - prints the checksums, diff --git a/src/halmos/traces.py b/src/dolmos/traces.py similarity index 95% rename from src/halmos/traces.py rename to src/dolmos/traces.py index 1a24c19c..1f328ec9 100644 --- a/src/halmos/traces.py +++ b/src/dolmos/traces.py @@ -6,11 +6,11 @@ from z3 import Z3_OP_CONCAT, BitVecNumRef, BitVecRef, is_app -from halmos.bytevec import ByteVec -from halmos.config import Config, TraceEvent -from halmos.exceptions import HalmosException -from halmos.mapper import DeployAddressMapper, Mapper -from halmos.sevm import ( +from dolmos.bytevec import ByteVec +from dolmos.config import Config, TraceEvent +from dolmos.exceptions import DolmosException +from dolmos.mapper import DeployAddressMapper, Mapper +from dolmos.sevm import ( CallContext, CallSequence, EventLog, @@ -18,7 +18,7 @@ StorageWrite, mnemonic, ) -from halmos.utils import ( +from dolmos.utils import ( Address, byte_length, cyan, @@ -158,7 +158,7 @@ def rendered_calldata(calldata: ByteVec, contract_name: str | None = None) -> st def render_trace(context: CallContext, file=sys.stdout) -> None: config: Config = config_context.get() if config is None: - raise HalmosException("config not set") + raise DolmosException("config not set") message = context.message addr_str = rendered_address(message.target) @@ -219,7 +219,7 @@ def render_trace(context: CallContext, file=sys.stdout) -> None: if TraceEvent.SSTORE in config.trace_events: print(f"{log_indent}{rendered_sstore(trace_element)}", file=file) case _: - raise HalmosException(f"unexpected trace element: {trace_element}") + raise DolmosException(f"unexpected trace element: {trace_element}") render_output(context, file=file) diff --git a/src/halmos/ui.py b/src/dolmos/ui.py similarity index 100% rename from src/halmos/ui.py rename to src/dolmos/ui.py diff --git a/src/halmos/utils.py b/src/dolmos/utils.py similarity index 99% rename from src/halmos/utils.py rename to src/dolmos/utils.py index 403b23a1..b8926f91 100644 --- a/src/halmos/utils.py +++ b/src/dolmos/utils.py @@ -43,12 +43,12 @@ substitute, ) -from halmos.bitvec import HalmosBitVec as BV -from halmos.bitvec import HalmosBool as Bool -from halmos.exceptions import HalmosException, NotConcreteError -from halmos.hashes import keccak256_256, keccak256_512 -from halmos.logs import warn -from halmos.mapper import Mapper +from dolmos.bitvec import DolmosBitVec as BV +from dolmos.bitvec import DolmosBool as Bool +from dolmos.exceptions import DolmosException, NotConcreteError +from dolmos.hashes import keccak256_256, keccak256_512 +from dolmos.logs import warn +from dolmos.mapper import Mapper # order of the secp256k1 curve secp256k1n = ( @@ -56,7 +56,7 @@ ) if TYPE_CHECKING: - from halmos.bytevec import ByteVec + from dolmos.bytevec import ByteVec Byte = int | BitVecRef | BV # uint8 Bytes4 = int | BitVecRef | BV # uint32 @@ -437,7 +437,7 @@ 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()}") + raise DolmosException(f"byte_length({x}) with bit size {x.size()}") return math.ceil(x.size() / 8) raise TypeError(f"byte_length({x}) of type {type(x)}") diff --git a/tests/conftest.py b/tests/conftest.py index 5b057506..f3658cad 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,8 +5,8 @@ def pytest_addoption(parser): parser.addoption( - "--halmos-options", + "--dolmos-options", metavar="OPTIONS", default="", - help="Halmos commandline options", + help="dolmos command line options", ) diff --git a/tests/expected/all.json b/tests/expected/all.json index 50fd3ab3..ed92c1e4 100644 --- a/tests/expected/all.json +++ b/tests/expected/all.json @@ -528,7 +528,7 @@ "num_bounded_loops": null }, { - "name": "check_call0_halmos_exception()", + "name": "check_call0_dolmos_exception()", "exitcode": 3, "num_models": 0, "models": null, @@ -555,7 +555,7 @@ "num_bounded_loops": null }, { - "name": "check_call1_halmos_exception(uint256)", + "name": "check_call1_dolmos_exception(uint256)", "exitcode": 3, "num_models": 0, "models": null, @@ -582,7 +582,7 @@ "num_bounded_loops": null }, { - "name": "check_create_halmos_exception()", + "name": "check_create_dolmos_exception()", "exitcode": 3, "num_models": 0, "models": null, @@ -1380,10 +1380,10 @@ "num_bounded_loops": null } ], - "test/HalmosCheatCode.t.sol:HalmosCheatCodeTest": [ + "test/Invariant.t.sol:InvariantProxyTest": [ { - "name": "check_FailUnknownCheatcode()", - "exitcode": 3, + "name": "invariant_proxy_1()", + "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, @@ -1391,16 +1391,18 @@ "num_bounded_loops": null }, { - "name": "check_SymbolLabel()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_proxy_2()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/Invariant.t.sol:InvariantTest": [ { - "name": "check_createAddress()", + "name": "invariant_1()", "exitcode": 0, "num_models": 0, "models": null, @@ -1409,61 +1411,73 @@ "num_bounded_loops": null }, { - "name": "check_createBool()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_2()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/InvariantProbes.t.sol:InvariantProbesTest": [ { - "name": "check_createBytes()", + "name": "invariant_probes_found()", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/InvariantSender.t.sol:InvariantSenderTest_all": [ { - "name": "check_createBytes32()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_num()", + "exitcode": 1, + "num_models": 2, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/InvariantSender.t.sol:InvariantSenderTest_non_owner_1": [ { - "name": "check_createBytes4_basic()", + "name": "invariant_num()", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/InvariantSender.t.sol:InvariantSenderTest_non_owner_2": [ { - "name": "check_createBytes4_finds_selector()", - "exitcode": 1, - "num_models": 1, + "name": "invariant_num()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/InvariantSender.t.sol:InvariantSenderTest_owner": [ { - "name": "check_createBytes_empty()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_num()", + "exitcode": 1, + "num_models": 2, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/InvariantSender2.t.sol:InvariantSenderTest_excludeSender_0": [ { - "name": "check_createInt()", + "name": "invariant_num1()", "exitcode": 0, "num_models": 0, "models": null, @@ -1472,34 +1486,36 @@ "num_bounded_loops": null }, { - "name": "check_createInt256()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_num2()", + "exitcode": 1, + "num_models": 4, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_createString()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_num3()", + "exitcode": 1, + "num_models": 4, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/InvariantSender2.t.sol:InvariantSenderTest_excludeSender_1": [ { - "name": "check_createString_empty()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_num1()", + "exitcode": 1, + "num_models": 2, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_createUint()", + "name": "invariant_num2()", "exitcode": 0, "num_models": 0, "models": null, @@ -1508,26 +1524,28 @@ "num_bounded_loops": null }, { - "name": "check_createUint256()", + "name": "invariant_num3()", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/InvariantSender2.t.sol:InvariantSenderTest_excludeSender_2": [ { - "name": "check_enableSymbolicStorage_fail()", + "name": "invariant_num1()", "exitcode": 1, - "num_models": 1, + "num_models": 2, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_enableSymbolicStorage_nonexistent()", - "exitcode": 3, + "name": "invariant_num2()", + "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, @@ -1535,16 +1553,18 @@ "num_bounded_loops": null }, { - "name": "check_enableSymbolicStorage_pass(uint256)", + "name": "invariant_num3()", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/InvariantSender2.t.sol:InvariantSenderTest_excludeSender_3": [ { - "name": "check_env_address()", + "name": "invariant_num1()", "exitcode": 0, "num_models": 0, "models": null, @@ -1553,7 +1573,7 @@ "num_bounded_loops": null }, { - "name": "check_env_address_array()", + "name": "invariant_num2()", "exitcode": 0, "num_models": 0, "models": null, @@ -1562,52 +1582,56 @@ "num_bounded_loops": null }, { - "name": "check_env_bool()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_num3()", + "exitcode": 1, + "num_models": 2, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/InvariantSender2.t.sol:InvariantSenderTest_excludeSender_empty": [ { - "name": "check_env_bool_array()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_num1()", + "exitcode": 1, + "num_models": 6, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_env_bytes()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_num2()", + "exitcode": 1, + "num_models": 6, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_env_bytes32()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_num3()", + "exitcode": 1, + "num_models": 6, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/InvariantTarget.t.sol:InvariantTargetTest_0": [ { - "name": "check_env_bytes32_array()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_0()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_env_bytes_array()", + "name": "invariant_1()", "exitcode": 0, "num_models": 0, "models": null, @@ -1616,7 +1640,7 @@ "num_bounded_loops": null }, { - "name": "check_env_exists()", + "name": "invariant_2()", "exitcode": 0, "num_models": 0, "models": null, @@ -1625,7 +1649,7 @@ "num_bounded_loops": null }, { - "name": "check_env_int()", + "name": "invariant_3()", "exitcode": 0, "num_models": 0, "models": null, @@ -1634,61 +1658,65 @@ "num_bounded_loops": null }, { - "name": "check_env_int_array()", + "name": "invariant_4()", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/InvariantTarget.t.sol:InvariantTargetTest_0_1_2_3": [ { - "name": "check_env_missing_fail()", - "exitcode": 5, - "num_models": null, + "name": "invariant_0()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_env_or_address()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_1()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_env_or_address_array()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_2()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_env_or_address_array_without_env_var()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_3()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_env_or_address_without_env_var()", + "name": "invariant_4()", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/InvariantTarget.t.sol:InvariantTargetTest_1_2": [ { - "name": "check_env_or_bool()", + "name": "invariant_0()", "exitcode": 0, "num_models": 0, "models": null, @@ -1697,25 +1725,25 @@ "num_bounded_loops": null }, { - "name": "check_env_or_bool_array()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_1()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_env_or_bool_array_without_env_var()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_2()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_env_or_bool_without_env_var(bool)", + "name": "invariant_3()", "exitcode": 0, "num_models": 0, "models": null, @@ -1724,16 +1752,18 @@ "num_bounded_loops": null }, { - "name": "check_env_or_bytes()", + "name": "invariant_4()", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/InvariantTarget.t.sol:InvariantTargetTest_3": [ { - "name": "check_env_or_bytes32()", + "name": "invariant_0()", "exitcode": 0, "num_models": 0, "models": null, @@ -1742,7 +1772,7 @@ "num_bounded_loops": null }, { - "name": "check_env_or_bytes32_array()", + "name": "invariant_1()", "exitcode": 0, "num_models": 0, "models": null, @@ -1751,7 +1781,7 @@ "num_bounded_loops": null }, { - "name": "check_env_or_bytes32_array_without_env_var()", + "name": "invariant_2()", "exitcode": 0, "num_models": 0, "models": null, @@ -1760,79 +1790,83 @@ "num_bounded_loops": null }, { - "name": "check_env_or_bytes32_without_env_var()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_3()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_env_or_bytes_array()", + "name": "invariant_4()", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/InvariantTarget.t.sol:InvariantTargetTest_all": [ { - "name": "check_env_or_bytes_array_without_env_var()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_0()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_env_or_bytes_without_env_var()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_1()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_env_or_int(int256)", - "exitcode": 0, - "num_models": 0, - "models": null, - "num_paths": null, + "name": "invariant_2()", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_env_or_int_array()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_3()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_env_or_int_array_without_env_var()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_4()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/InvariantTarget.t.sol:InvariantTargetTest_target_contract_this": [ { - "name": "check_env_or_string()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_0()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_env_or_string_array()", + "name": "invariant_1()", "exitcode": 0, "num_models": 0, "models": null, @@ -1841,7 +1875,7 @@ "num_bounded_loops": null }, { - "name": "check_env_or_string_array_without_env_var()", + "name": "invariant_2()", "exitcode": 0, "num_models": 0, "models": null, @@ -1850,7 +1884,7 @@ "num_bounded_loops": null }, { - "name": "check_env_or_string_without_env_var()", + "name": "invariant_3()", "exitcode": 0, "num_models": 0, "models": null, @@ -1859,16 +1893,18 @@ "num_bounded_loops": null }, { - "name": "check_env_or_uint(uint256)", + "name": "invariant_4()", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/InvariantTarget2.t.sol:InvariantTargetTest_excludeContract_0_0": [ { - "name": "check_env_or_uint_array()", + "name": "invariant_0()", "exitcode": 0, "num_models": 0, "models": null, @@ -1877,53 +1913,55 @@ "num_bounded_loops": null }, { - "name": "check_env_or_uint_array_without_env_var()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_1()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_env_string()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_2()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_env_string_array()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_3()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_env_uint()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_4()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/InvariantTarget2.t.sol:InvariantTargetTest_excludeContract_0_1": [ { - "name": "check_env_uint_array()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_0()", + "exitcode": 5, + "num_models": null, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_extract_string_argument_fail(string)", - "exitcode": 3, + "name": "invariant_1()", + "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, @@ -1931,7 +1969,7 @@ "num_bounded_loops": null }, { - "name": "check_randomAddress()", + "name": "invariant_2()", "exitcode": 0, "num_models": 0, "models": null, @@ -1940,7 +1978,7 @@ "num_bounded_loops": null }, { - "name": "check_randomBool()", + "name": "invariant_3()", "exitcode": 0, "num_models": 0, "models": null, @@ -1949,16 +1987,18 @@ "num_bounded_loops": null }, { - "name": "check_randomBytes()", + "name": "invariant_4()", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/InvariantTarget2.t.sol:InvariantTargetTest_excludeContract_0_2": [ { - "name": "check_randomBytes4()", + "name": "invariant_0()", "exitcode": 0, "num_models": 0, "models": null, @@ -1967,7 +2007,7 @@ "num_bounded_loops": null }, { - "name": "check_randomBytes8()", + "name": "invariant_1()", "exitcode": 0, "num_models": 0, "models": null, @@ -1976,7 +2016,7 @@ "num_bounded_loops": null }, { - "name": "check_randomBytes_empty()", + "name": "invariant_2()", "exitcode": 0, "num_models": 0, "models": null, @@ -1985,16 +2025,27 @@ "num_bounded_loops": null }, { - "name": "check_randomInt()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_3()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_randomInt256()", + "name": "invariant_4()", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/InvariantTarget2.t.sol:InvariantTargetTest_excludeContract_0_3": [ + { + "name": "invariant_0()", "exitcode": 0, "num_models": 0, "models": null, @@ -2003,16 +2054,16 @@ "num_bounded_loops": null }, { - "name": "check_randomUint()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_1()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_randomUint256()", + "name": "invariant_2()", "exitcode": 0, "num_models": 0, "models": null, @@ -2021,7 +2072,7 @@ "num_bounded_loops": null }, { - "name": "check_random_uint_range_concrete_pass()", + "name": "invariant_3()", "exitcode": 0, "num_models": 0, "models": null, @@ -2030,17 +2081,19 @@ "num_bounded_loops": null }, { - "name": "check_random_uint_range_max_greaterthan_min_fail()", - "exitcode": 3, + "name": "invariant_4()", + "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/InvariantTarget2.t.sol:InvariantTargetTest_excludeContract_0_4": [ { - "name": "check_random_uint_range_symbolic_fail(uint256,uint256)", - "exitcode": 3, + "name": "invariant_0()", + "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, @@ -2048,7 +2101,7 @@ "num_bounded_loops": null }, { - "name": "check_setArbitraryStorage_fail()", + "name": "invariant_1()", "exitcode": 1, "num_models": 1, "models": null, @@ -2057,8 +2110,8 @@ "num_bounded_loops": null }, { - "name": "check_setArbitraryStorage_nonexistent()", - "exitcode": 3, + "name": "invariant_2()", + "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, @@ -2066,28 +2119,28 @@ "num_bounded_loops": null }, { - "name": "check_setArbitraryStorage_pass(uint256)", - "exitcode": 0, - "num_models": 0, + "name": "invariant_3()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_unsupported_cheatcode_fail()", - "exitcode": 3, - "num_models": 0, + "name": "invariant_4()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null } ], - "test/HalmosCheatCode.t.sol:HalmosCreateCalldataTest": [ + "test/InvariantTarget2.t.sol:InvariantTargetTest_excludeContract_1": [ { - "name": "check_createCalldata_Beep_0_excluding_pure()", - "exitcode": 4, + "name": "invariant_0()", + "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, @@ -2095,25 +2148,25 @@ "num_bounded_loops": null }, { - "name": "check_createCalldata_Beep_0_including_pure()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_1()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_createCalldata_Beep_1_excluding_pure()", - "exitcode": 4, - "num_models": 0, + "name": "invariant_2()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_createCalldata_Beep_1_including_pure()", + "name": "invariant_3()", "exitcode": 0, "num_models": 0, "models": null, @@ -2122,16 +2175,18 @@ "num_bounded_loops": null }, { - "name": "check_createCalldata_Beep_2_excluding_pure()", - "exitcode": 4, + "name": "invariant_4()", + "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/InvariantTarget2.t.sol:InvariantTargetTest_excludeContract_2": [ { - "name": "check_createCalldata_Beep_2_including_pure()", + "name": "invariant_0()", "exitcode": 0, "num_models": 0, "models": null, @@ -2140,88 +2195,92 @@ "num_bounded_loops": null }, { - "name": "check_createCalldata_Dummy_fail()", - "exitcode": 3, - "num_models": 0, + "name": "invariant_1()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_createCalldata_Fallback()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_2()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_createCalldata_Mock_0_excluding_view_fail()", - "exitcode": 1, - "num_models": 18, + "name": "invariant_3()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_createCalldata_Mock_0_excluding_view_pass()", + "name": "invariant_4()", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/InvariantTarget2.t.sol:InvariantTargetTest_excludeSelector_1": [ { - "name": "check_createCalldata_Mock_0_including_view_fail()", - "exitcode": 1, - "num_models": 20, + "name": "invariant_0()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_createCalldata_Mock_0_including_view_pass()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_1()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_createCalldata_Mock_1_fail()", + "name": "invariant_2()", "exitcode": 1, - "num_models": 18, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_createCalldata_Mock_1_pass()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_3()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_createCalldata_Mock_2_excluding_view_fail()", + "name": "invariant_4()", "exitcode": 1, - "num_models": 18, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/InvariantTarget2.t.sol:InvariantTargetTest_excludeSelector_2": [ { - "name": "check_createCalldata_Mock_2_excluding_view_pass()", + "name": "invariant_0()", "exitcode": 0, "num_models": 0, "models": null, @@ -2230,52 +2289,54 @@ "num_bounded_loops": null }, { - "name": "check_createCalldata_Mock_2_including_view_fail()", + "name": "invariant_1()", "exitcode": 1, - "num_models": 20, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_createCalldata_Mock_2_including_view_pass()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_2()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_createCalldata_Mock_3_fail()", + "name": "invariant_3()", "exitcode": 1, - "num_models": 18, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_createCalldata_Mock_3_pass()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_4()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/InvariantTarget2.t.sol:InvariantTargetTest_excludeSelector_3": [ { - "name": "check_createCalldata_Mock_4_excluding_view_fail()", - "exitcode": 1, - "num_models": 18, + "name": "invariant_0()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_createCalldata_Mock_4_excluding_view_pass()", + "name": "invariant_1()", "exitcode": 0, "num_models": 0, "models": null, @@ -2284,34 +2345,36 @@ "num_bounded_loops": null }, { - "name": "check_createCalldata_Mock_4_including_view_fail()", + "name": "invariant_2()", "exitcode": 1, - "num_models": 20, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_createCalldata_Mock_4_including_view_pass()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_3()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_createCalldata_Mock_interface_excluding_view_fail()", + "name": "invariant_4()", "exitcode": 1, - "num_models": 18, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/InvariantTargetThis.t.sol:InvariantTargetBase": [ { - "name": "check_createCalldata_Mock_interface_excluding_view_pass()", + "name": "check_foo()", "exitcode": 0, "num_models": 0, "models": null, @@ -2320,16 +2383,27 @@ "num_bounded_loops": null }, { - "name": "check_createCalldata_Mock_interface_including_view_fail()", - "exitcode": 1, - "num_models": 20, + "name": "check_foo_called()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_createCalldata_Mock_interface_including_view_pass()", + "name": "invariant_this()", + "exitcode": 5, + "num_models": null, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/InvariantTargetThis.t.sol:InvariantTargetThis": [ + { + "name": "check_foo()", "exitcode": 0, "num_models": 0, "models": null, @@ -2338,131 +2412,126 @@ "num_bounded_loops": null }, { - "name": "check_createCalldata_NestedArrays_1_fail()", - "exitcode": 1, - "num_models": 541, + "name": "check_foo_called()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_createCalldata_NestedArrays_1_pass()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_targets_foo_included()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_createCalldata_unknown_fail()", - "exitcode": 3, - "num_models": 0, + "name": "invariant_targets_inception_afterInvariant_included()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/Invariant.t.sol:InvariantProxyTest": [ + }, { - "name": "invariant_proxy_1()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_targets_inception_check_included()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_proxy_2()", + "name": "invariant_targets_inception_invariant_included()", "exitcode": 1, "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/Invariant.t.sol:InvariantTest": [ + }, { - "name": "invariant_1()", - "exitcode": 0, - "num_models": 0, + "name": "invariant_targets_inception_setUp_included()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_2()", + "name": "invariant_targets_inception_test_included()", "exitcode": 1, "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/InvariantProbes.t.sol:InvariantProbesTest": [ + }, { - "name": "invariant_probes_found()", + "name": "invariant_targets_special_functions_excluded()", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/InvariantSender.t.sol:InvariantSenderTest_all": [ + }, { - "name": "invariant_num()", - "exitcode": 1, - "num_models": 2, + "name": "invariant_this()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null } ], - "test/InvariantSender.t.sol:InvariantSenderTest_non_owner_1": [ + "test/InvariantTimestamp.t.sol:InvariantTimestampTest": [ { - "name": "invariant_num()", + "name": "check_timestamp_setup()", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/InvariantSender.t.sol:InvariantSenderTest_non_owner_2": [ + }, { - "name": "invariant_num()", + "name": "invariant_timestamp_can_change()", + "exitcode": 1, + "num_models": 2, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "invariant_timestamp_can_only_move_forward()", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/InvariantSender.t.sol:InvariantSenderTest_owner": [ + }, { - "name": "invariant_num()", + "name": "invariant_timestamp_does_not_have_to_change()", "exitcode": 1, "num_models": 2, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/InvariantSender2.t.sol:InvariantSenderTest_excludeSender_0": [ + }, { - "name": "invariant_num1()", + "name": "invariant_timestamp_is_bounded()", "exitcode": 0, "num_models": 0, "models": null, @@ -2471,45 +2540,49 @@ "num_bounded_loops": null }, { - "name": "invariant_num2()", + "name": "invariant_timestamp_setup()", "exitcode": 1, - "num_models": 4, + "num_models": 3, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_num3()", + "name": "invariant_timestamp_timekeeper(address)", "exitcode": 1, - "num_models": 4, + "num_models": 2, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null } ], - "test/InvariantSender2.t.sol:InvariantSenderTest_excludeSender_1": [ + "test/Library.t.sol:LibraryTest": [ { - "name": "invariant_num1()", - "exitcode": 1, - "num_models": 2, + "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": "invariant_num2()", + "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": "invariant_num3()", + "name": "check_bar()", "exitcode": 0, "num_models": 0, "models": null, @@ -2518,18 +2591,18 @@ "num_bounded_loops": null } ], - "test/InvariantSender2.t.sol:InvariantSenderTest_excludeSender_2": [ + "test/List.t.sol:ListTest": [ { - "name": "invariant_num1()", - "exitcode": 1, - "num_models": 2, + "name": "check_add(uint256)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_num2()", + "name": "check_remove()", "exitcode": 0, "num_models": 0, "models": null, @@ -2538,7 +2611,7 @@ "num_bounded_loops": null }, { - "name": "invariant_num3()", + "name": "check_set(uint256,uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -2547,9 +2620,9 @@ "num_bounded_loops": null } ], - "test/InvariantSender2.t.sol:InvariantSenderTest_excludeSender_3": [ + "test/List.t.sol:ListTestTest": [ { - "name": "invariant_num1()", + "name": "check_add(uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -2558,7 +2631,7 @@ "num_bounded_loops": null }, { - "name": "invariant_num2()", + "name": "check_remove()", "exitcode": 0, "num_models": 0, "models": null, @@ -2567,56 +2640,54 @@ "num_bounded_loops": null }, { - "name": "invariant_num3()", - "exitcode": 1, - "num_models": 2, + "name": "check_set(uint256,uint256)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null } ], - "test/InvariantSender2.t.sol:InvariantSenderTest_excludeSender_empty": [ + "test/MegaMem.t.sol:MegaMemTest": [ { - "name": "invariant_num1()", - "exitcode": 1, - "num_models": 6, + "name": "check_megaMem_call_in_len_reverts()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_num2()", - "exitcode": 1, - "num_models": 6, + "name": "check_megaMem_call_in_ptr_reverts(bool)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_num3()", - "exitcode": 1, - "num_models": 6, + "name": "check_megaMem_call_out_len_ok()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/InvariantTarget.t.sol:InvariantTargetTest_0": [ + }, { - "name": "invariant_0()", - "exitcode": 1, - "num_models": 1, + "name": "check_megaMem_call_out_ptr_reverts(bool)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_1()", + "name": "check_megaMem_calldatacopy_dst_ptr_reverts(bool)", "exitcode": 0, "num_models": 0, "models": null, @@ -2625,7 +2696,7 @@ "num_bounded_loops": null }, { - "name": "invariant_2()", + "name": "check_megaMem_calldatacopy_src_len_reverts()", "exitcode": 0, "num_models": 0, "models": null, @@ -2634,7 +2705,7 @@ "num_bounded_loops": null }, { - "name": "invariant_3()", + "name": "check_megaMem_calldatacopy_src_ptr_ok()", "exitcode": 0, "num_models": 0, "models": null, @@ -2643,65 +2714,61 @@ "num_bounded_loops": null }, { - "name": "invariant_4()", + "name": "check_megaMem_calldataload_ok()", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/InvariantTarget.t.sol:InvariantTargetTest_0_1_2_3": [ + }, { - "name": "invariant_0()", - "exitcode": 1, - "num_models": 1, + "name": "check_megaMem_codecopy_dst_ptr_reverts(bool)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_1()", - "exitcode": 1, - "num_models": 1, + "name": "check_megaMem_codecopy_src_len_reverts()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_2()", - "exitcode": 1, - "num_models": 1, + "name": "check_megaMem_codecopy_src_ptr_ok()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_3()", - "exitcode": 1, - "num_models": 1, + "name": "check_megaMem_create2_src_len_reverts()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_4()", + "name": "check_megaMem_create2_src_ptr_reverts()", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/InvariantTarget.t.sol:InvariantTargetTest_1_2": [ + }, { - "name": "invariant_0()", + "name": "check_megaMem_create_src_len_reverts()", "exitcode": 0, "num_models": 0, "models": null, @@ -2710,25 +2777,25 @@ "num_bounded_loops": null }, { - "name": "invariant_1()", - "exitcode": 1, - "num_models": 1, + "name": "check_megaMem_create_src_ptr_reverts()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_2()", - "exitcode": 1, - "num_models": 1, + "name": "check_megaMem_extcodecopy_dst_ptr_reverts(bool)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_3()", + "name": "check_megaMem_extcodecopy_src_len_reverts()", "exitcode": 0, "num_models": 0, "models": null, @@ -2737,18 +2804,16 @@ "num_bounded_loops": null }, { - "name": "invariant_4()", + "name": "check_megaMem_extcodecopy_src_ptr_ok()", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/InvariantTarget.t.sol:InvariantTargetTest_3": [ + }, { - "name": "invariant_0()", + "name": "check_megaMem_keccak256_len_reverts()", "exitcode": 0, "num_models": 0, "models": null, @@ -2757,7 +2822,7 @@ "num_bounded_loops": null }, { - "name": "invariant_1()", + "name": "check_megaMem_keccak256_ptr_reverts(bool)", "exitcode": 0, "num_models": 0, "models": null, @@ -2766,7 +2831,7 @@ "num_bounded_loops": null }, { - "name": "invariant_2()", + "name": "check_megaMem_log0_len_reverts()", "exitcode": 0, "num_models": 0, "models": null, @@ -2775,83 +2840,79 @@ "num_bounded_loops": null }, { - "name": "invariant_3()", - "exitcode": 1, - "num_models": 1, + "name": "check_megaMem_log0_ptr_reverts()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_4()", + "name": "check_megaMem_mcopy_dst_ptr_reverts()", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/InvariantTarget.t.sol:InvariantTargetTest_all": [ + }, { - "name": "invariant_0()", - "exitcode": 1, - "num_models": 1, + "name": "check_megaMem_mcopy_src_len_reverts()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_1()", - "exitcode": 1, - "num_models": 1, + "name": "check_megaMem_mcopy_src_ptr_reverts()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_2()", - "exitcode": 1, - "num_models": 1, + "name": "check_megaMem_mload_src_ptr_reverts()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_3()", - "exitcode": 1, - "num_models": 1, + "name": "check_megaMem_mstore8_dst_ptr_reverts()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_4()", - "exitcode": 1, - "num_models": 1, + "name": "check_megaMem_mstore_dst_ptr_reverts()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/InvariantTarget.t.sol:InvariantTargetTest_target_contract_this": [ + }, { - "name": "invariant_0()", - "exitcode": 1, - "num_models": 1, + "name": "check_megaMem_new_bytes_reverts()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_1()", + "name": "check_megaMem_return_len_reverts()", "exitcode": 0, "num_models": 0, "models": null, @@ -2860,7 +2921,7 @@ "num_bounded_loops": null }, { - "name": "invariant_2()", + "name": "check_megaMem_return_ptr_reverts()", "exitcode": 0, "num_models": 0, "models": null, @@ -2869,7 +2930,7 @@ "num_bounded_loops": null }, { - "name": "invariant_3()", + "name": "check_megaMem_returndatacopy_dst_ptr_reverts(bool)", "exitcode": 0, "num_models": 0, "models": null, @@ -2878,18 +2939,16 @@ "num_bounded_loops": null }, { - "name": "invariant_4()", + "name": "check_megaMem_returndatacopy_src_len_reverts()", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/InvariantTarget2.t.sol:InvariantTargetTest_excludeContract_0_0": [ + }, { - "name": "invariant_0()", + "name": "check_megaMem_returndatacopy_src_ptr_reverts()", "exitcode": 0, "num_models": 0, "models": null, @@ -2898,34 +2957,36 @@ "num_bounded_loops": null }, { - "name": "invariant_1()", - "exitcode": 1, - "num_models": 1, + "name": "check_megaMem_revert_len_reverts()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_2()", - "exitcode": 1, - "num_models": 1, + "name": "check_megaMem_revert_ptr_reverts()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/Natspec.t.sol:NatspecTestContract": [ { - "name": "invariant_3()", - "exitcode": 1, - "num_models": 1, + "name": "check_Loop3(uint256)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_4()", + "name": "check_Loop3Fail(uint256)", "exitcode": 1, "num_models": 1, "models": null, @@ -2934,18 +2995,9 @@ "num_bounded_loops": null } ], - "test/InvariantTarget2.t.sol:InvariantTargetTest_excludeContract_0_1": [ - { - "name": "invariant_0()", - "exitcode": 5, - "num_models": null, - "models": null, - "num_paths": null, - "time": null, - "num_bounded_loops": null - }, + "test/Natspec.t.sol:NatspecTestFunction": [ { - "name": "invariant_1()", + "name": "check_Loop2(uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -2954,16 +3006,16 @@ "num_bounded_loops": null }, { - "name": "invariant_2()", - "exitcode": 0, - "num_models": 0, + "name": "check_Loop2Fail(uint256)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_3()", + "name": "check_Loop3(uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -2972,18 +3024,18 @@ "num_bounded_loops": null }, { - "name": "invariant_4()", - "exitcode": 0, - "num_models": 0, + "name": "check_Loop3Fail(uint256)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null } ], - "test/InvariantTarget2.t.sol:InvariantTargetTest_excludeContract_0_2": [ + "test/Natspec.t.sol:NatspecTestLegacyTag": [ { - "name": "invariant_0()", + "name": "check_Loop3(uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -2992,34 +3044,36 @@ "num_bounded_loops": null }, { - "name": "invariant_1()", - "exitcode": 0, - "num_models": 0, + "name": "check_Loop3Fail(uint256)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_2()", + "name": "check_Loop4(uint256)", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/Natspec.t.sol:NatspecTestNone": [ { - "name": "invariant_3()", - "exitcode": 1, - "num_models": 1, + "name": "check_Loop2(uint256)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_4()", + "name": "check_Loop2Fail(uint256)", "exitcode": 1, "num_models": 1, "models": null, @@ -3028,9 +3082,9 @@ "num_bounded_loops": null } ], - "test/InvariantTarget2.t.sol:InvariantTargetTest_excludeContract_0_3": [ + "test/Natspec.t.sol:NatspecTestOverwrite": [ { - "name": "invariant_0()", + "name": "check_Loop3(uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -3039,7 +3093,7 @@ "num_bounded_loops": null }, { - "name": "invariant_1()", + "name": "check_Loop3Fail(uint256)", "exitcode": 1, "num_models": 1, "models": null, @@ -3048,7 +3102,7 @@ "num_bounded_loops": null }, { - "name": "invariant_2()", + "name": "check_Loop4(uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -3057,7 +3111,18 @@ "num_bounded_loops": null }, { - "name": "invariant_3()", + "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, @@ -3066,18 +3131,18 @@ "num_bounded_loops": null }, { - "name": "invariant_4()", - "exitcode": 0, - "num_models": 0, + "name": "check_Loop2Fail(uint256)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null } ], - "test/InvariantTarget2.t.sol:InvariantTargetTest_excludeContract_0_4": [ + "test/OpCodesBlobBaseFee_BlobHash.t.sol:BlobBaseFeeTest": [ { - "name": "invariant_0()", + "name": "check_blobbasefee_affordable(uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -3086,16 +3151,16 @@ "num_bounded_loops": null }, { - "name": "invariant_1()", - "exitcode": 1, - "num_models": 1, + "name": "check_blobbasefee_no_revert()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_2()", + "name": "check_blobbasefee_solidity_matches_opcode()", "exitcode": 0, "num_models": 0, "models": null, @@ -3104,27 +3169,27 @@ "num_bounded_loops": null }, { - "name": "invariant_3()", - "exitcode": 1, - "num_models": 1, + "name": "check_blobbasefee_zero_in_test_env()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/OpCodesBlobBaseFee_BlobHash.t.sol:BlobHashTest": [ { - "name": "invariant_4()", - "exitcode": 1, - "num_models": 1, + "name": "check_blobhash_contiguous(uint8)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/InvariantTarget2.t.sol:InvariantTargetTest_excludeContract_1": [ + }, { - "name": "invariant_0()", + "name": "check_blobhash_deterministic(uint8)", "exitcode": 0, "num_models": 0, "models": null, @@ -3133,25 +3198,25 @@ "num_bounded_loops": null }, { - "name": "invariant_1()", - "exitcode": 1, - "num_models": 1, + "name": "check_blobhash_no_revert(uint8)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_2()", - "exitcode": 1, - "num_models": 1, + "name": "check_blobhash_out_of_range()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_3()", + "name": "check_blobhash_version_byte(uint8)", "exitcode": 0, "num_models": 0, "models": null, @@ -3160,7 +3225,7 @@ "num_bounded_loops": null }, { - "name": "invariant_4()", + "name": "check_blobhash_zero_no_blob_tx()", "exitcode": 0, "num_models": 0, "models": null, @@ -3169,9 +3234,9 @@ "num_bounded_loops": null } ], - "test/InvariantTarget2.t.sol:InvariantTargetTest_excludeContract_2": [ + "test/OpCodesCLZ.sol:ClzTest": [ { - "name": "invariant_0()", + "name": "check_clz_byte()", "exitcode": 0, "num_models": 0, "models": null, @@ -3180,25 +3245,25 @@ "num_bounded_loops": null }, { - "name": "invariant_1()", - "exitcode": 1, - "num_models": 1, + "name": "check_clz_lower_bound(uint256)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_2()", - "exitcode": 1, - "num_models": 1, + "name": "check_clz_max()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_3()", + "name": "check_clz_monotone(uint256,uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -3207,18 +3272,16 @@ "num_bounded_loops": null }, { - "name": "invariant_4()", + "name": "check_clz_msb()", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/InvariantTarget2.t.sol:InvariantTargetTest_excludeSelector_1": [ + }, { - "name": "invariant_0()", + "name": "check_clz_one()", "exitcode": 0, "num_models": 0, "models": null, @@ -3227,45 +3290,34 @@ "num_bounded_loops": null }, { - "name": "invariant_1()", - "exitcode": 1, - "num_models": 1, + "name": "check_clz_power_of_two(uint8)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_2()", - "exitcode": 1, - "num_models": 1, + "name": "check_clz_range(uint256)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_3()", - "exitcode": 1, - "num_models": 1, + "name": "check_clz_two()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_4()", - "exitcode": 1, - "num_models": 1, - "models": null, - "num_paths": null, - "time": null, - "num_bounded_loops": null - } - ], - "test/InvariantTarget2.t.sol:InvariantTargetTest_excludeSelector_2": [ - { - "name": "invariant_0()", + "name": "check_clz_upper_bound(uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -3274,54 +3326,56 @@ "num_bounded_loops": null }, { - "name": "invariant_1()", - "exitcode": 1, - "num_models": 1, + "name": "check_clz_zero()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_2()", - "exitcode": 1, - "num_models": 1, + "name": "check_clz_zero_symbolic()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/Opcode.t.sol:OpcodeTest": [ { - "name": "invariant_3()", - "exitcode": 1, - "num_models": 1, + "name": "check_PUSH0()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_4()", - "exitcode": 1, - "num_models": 1, + "name": "check_SIGNEXTEND(uint256)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null } ], - "test/InvariantTarget2.t.sol:InvariantTargetTest_excludeSelector_3": [ + "test/Panic.t.sol:PanicTest": [ { - "name": "invariant_0()", - "exitcode": 0, - "num_models": 0, + "name": "check_panic_0_fail(bool)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_1()", + "name": "check_panic_0_pass(bool)", "exitcode": 0, "num_models": 0, "models": null, @@ -3330,7 +3384,7 @@ "num_bounded_loops": null }, { - "name": "invariant_2()", + "name": "check_panic_0x11_fail(uint256)", "exitcode": 1, "num_models": 1, "models": null, @@ -3339,27 +3393,25 @@ "num_bounded_loops": null }, { - "name": "invariant_3()", - "exitcode": 1, - "num_models": 1, + "name": "check_panic_0x11_pass(uint256)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_4()", + "name": "check_panic_0x12_fail(uint256)", "exitcode": 1, "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/InvariantTargetThis.t.sol:InvariantTargetBase": [ + }, { - "name": "check_foo()", + "name": "check_panic_0x12_pass(uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -3368,36 +3420,34 @@ "num_bounded_loops": null }, { - "name": "check_foo_called()", - "exitcode": 0, - "num_models": 0, + "name": "check_panic_10_fail(bool)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_this()", - "exitcode": 5, - "num_models": null, + "name": "check_panic_16_fail(bool)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/InvariantTargetThis.t.sol:InvariantTargetThis": [ + }, { - "name": "check_foo()", - "exitcode": 0, - "num_models": 0, + "name": "check_panic_1_fail(bool)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_foo_called()", + "name": "check_panic_1_pass(bool)", "exitcode": 0, "num_models": 0, "models": null, @@ -3406,7 +3456,7 @@ "num_bounded_loops": null }, { - "name": "invariant_targets_foo_included()", + "name": "check_panic_2_or_3_fail_2(bool)", "exitcode": 1, "num_models": 1, "models": null, @@ -3415,7 +3465,7 @@ "num_bounded_loops": null }, { - "name": "invariant_targets_inception_afterInvariant_included()", + "name": "check_panic_2_or_3_fail_3(bool)", "exitcode": 1, "num_models": 1, "models": null, @@ -3424,7 +3474,7 @@ "num_bounded_loops": null }, { - "name": "invariant_targets_inception_check_included()", + "name": "check_panic_5_fail_old(bool)", "exitcode": 1, "num_models": 1, "models": null, @@ -3433,7 +3483,7 @@ "num_bounded_loops": null }, { - "name": "invariant_targets_inception_invariant_included()", + "name": "check_panic_7_fail(bool)", "exitcode": 1, "num_models": 1, "models": null, @@ -3442,7 +3492,7 @@ "num_bounded_loops": null }, { - "name": "invariant_targets_inception_setUp_included()", + "name": "check_panic_8_fail(bool)", "exitcode": 1, "num_models": 1, "models": null, @@ -3451,7 +3501,7 @@ "num_bounded_loops": null }, { - "name": "invariant_targets_inception_test_included()", + "name": "check_panic_any_fail_4(bool)", "exitcode": 1, "num_models": 1, "models": null, @@ -3460,27 +3510,16 @@ "num_bounded_loops": null }, { - "name": "invariant_targets_special_functions_excluded()", - "exitcode": 0, - "num_models": 0, + "name": "check_panic_inc_assert_fail(uint256)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_this()", - "exitcode": 0, - "num_models": 0, - "models": null, - "num_paths": null, - "time": null, - "num_bounded_loops": null - } - ], - "test/InvariantTimestamp.t.sol:InvariantTimestampTest": [ - { - "name": "check_timestamp_setup()", + "name": "check_panic_inc_assert_pass(uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -3489,63 +3528,45 @@ "num_bounded_loops": null }, { - "name": "invariant_timestamp_can_change()", + "name": "check_panic_inc_cheatcode_fail(uint256)", "exitcode": 1, - "num_models": 2, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_timestamp_can_only_move_forward()", - "exitcode": 0, - "num_models": 0, + "name": "check_panic_inc_fail(uint256)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_timestamp_does_not_have_to_change()", + "name": "check_panic_inc_overflow_fail(uint256)", "exitcode": 1, - "num_models": 2, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "invariant_timestamp_is_bounded()", + "name": "check_panic_inc_overflow_pass(uint256)", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, - { - "name": "invariant_timestamp_setup()", - "exitcode": 1, - "num_models": 3, - "models": null, - "num_paths": null, - "time": null, - "num_bounded_loops": null - }, - { - "name": "invariant_timestamp_timekeeper(address)", - "exitcode": 1, - "num_models": 2, - "models": null, - "num_paths": null, - "time": null, - "num_bounded_loops": null } ], - "test/Library.t.sol:LibraryTest": [ + "test/Prank.t.sol:PrankSetUpTest": [ { - "name": "check_add(uint256,uint256)", + "name": "check_prank(address)", "exitcode": 0, "num_models": 0, "models": null, @@ -3554,31 +3575,27 @@ "num_bounded_loops": null } ], - "test/LibraryLinking.t.sol:LibTest": [ + "test/Prank.t.sol:PrankTest": [ { - "name": "check_foo()", + "name": "check_prank_Constructor(address,address)", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/LibraryLinking.t.sol:LibTest2": [ + }, { - "name": "check_bar()", + "name": "check_prank_ConstructorCreate2(address,address,bytes32)", "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)", + "name": "check_prank_External(address)", "exitcode": 0, "num_models": 0, "models": null, @@ -3587,7 +3604,7 @@ "num_bounded_loops": null }, { - "name": "check_remove()", + "name": "check_prank_ExternalSelf(address)", "exitcode": 0, "num_models": 0, "models": null, @@ -3596,18 +3613,16 @@ "num_bounded_loops": null }, { - "name": "check_set(uint256,uint256)", + "name": "check_prank_Internal(address)", "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)", + "name": "check_prank_New(address)", "exitcode": 0, "num_models": 0, "models": null, @@ -3616,7 +3631,7 @@ "num_bounded_loops": null }, { - "name": "check_remove()", + "name": "check_prank_Reset1(address)", "exitcode": 0, "num_models": 0, "models": null, @@ -3625,18 +3640,16 @@ "num_bounded_loops": null }, { - "name": "check_set(uint256,uint256)", + "name": "check_prank_Reset2(address)", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/MegaMem.t.sol:MegaMemTest": [ + }, { - "name": "check_megaMem_call_in_len_reverts()", + "name": "check_prank_double(address,address)", "exitcode": 0, "num_models": 0, "models": null, @@ -3645,7 +3658,7 @@ "num_bounded_loops": null }, { - "name": "check_megaMem_call_in_ptr_reverts(bool)", + "name": "check_prank_single(address)", "exitcode": 0, "num_models": 0, "models": null, @@ -3654,7 +3667,7 @@ "num_bounded_loops": null }, { - "name": "check_megaMem_call_out_len_ok()", + "name": "check_prank_startPrank_in_constructor(address,address)", "exitcode": 0, "num_models": 0, "models": null, @@ -3663,7 +3676,7 @@ "num_bounded_loops": null }, { - "name": "check_megaMem_call_out_ptr_reverts(bool)", + "name": "check_startPrank_double(address,address)", "exitcode": 0, "num_models": 0, "models": null, @@ -3672,7 +3685,7 @@ "num_bounded_loops": null }, { - "name": "check_megaMem_calldatacopy_dst_ptr_reverts(bool)", + "name": "check_startPrank_single(address)", "exitcode": 0, "num_models": 0, "models": null, @@ -3681,7 +3694,7 @@ "num_bounded_loops": null }, { - "name": "check_megaMem_calldatacopy_src_len_reverts()", + "name": "check_stopPrank_1(address)", "exitcode": 0, "num_models": 0, "models": null, @@ -3690,61 +3703,47 @@ "num_bounded_loops": null }, { - "name": "check_megaMem_calldatacopy_src_ptr_ok()", + "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_megaMem_calldataload_ok()", - "exitcode": 0, - "num_models": 0, - "models": null, - "num_paths": null, - "time": null, - "num_bounded_loops": null - }, - { - "name": "check_megaMem_codecopy_dst_ptr_reverts(bool)", - "exitcode": 0, - "num_models": 0, - "models": null, - "num_paths": null, - "time": null, - "num_bounded_loops": null - }, - { - "name": "check_megaMem_codecopy_src_len_reverts()", + "name": "check_foo(uint256,uint256,address)", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/Revert.t.sol:CTest": [ { - "name": "check_megaMem_codecopy_src_ptr_ok()", - "exitcode": 0, - "num_models": 0, + "name": "check_BalanceTransfer_Known(uint256,uint256)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_megaMem_create2_src_len_reverts()", - "exitcode": 0, - "num_models": 0, + "name": "check_BalanceTransfer_Unknown(uint256,uint256)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_megaMem_create2_src_ptr_reverts()", + "name": "check_Revert1(uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -3753,7 +3752,7 @@ "num_bounded_loops": null }, { - "name": "check_megaMem_create_src_len_reverts()", + "name": "check_Revert2(uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -3762,7 +3761,7 @@ "num_bounded_loops": null }, { - "name": "check_megaMem_create_src_ptr_reverts()", + "name": "check_RevertBalance_Known(bool,uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -3771,7 +3770,7 @@ "num_bounded_loops": null }, { - "name": "check_megaMem_extcodecopy_dst_ptr_reverts(bool)", + "name": "check_RevertBalance_Unknown(uint256,uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -3780,16 +3779,18 @@ "num_bounded_loops": null }, { - "name": "check_megaMem_extcodecopy_src_len_reverts()", + "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_megaMem_extcodecopy_src_ptr_ok()", + "name": "check_create(uint256,bytes32,uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -3798,7 +3799,7 @@ "num_bounded_loops": null }, { - "name": "check_megaMem_keccak256_len_reverts()", + "name": "check_send(uint256,uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -3807,52 +3808,63 @@ "num_bounded_loops": null }, { - "name": "check_megaMem_keccak256_ptr_reverts(bool)", + "name": "check_transfer(uint256)", "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_megaMem_log0_len_reverts()", + "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_megaMem_log0_ptr_reverts()", + "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_megaMem_mcopy_dst_ptr_reverts()", + "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_megaMem_mcopy_src_len_reverts()", + "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_megaMem_mcopy_src_ptr_reverts()", + "name": "check_address_collision_pass(uint256,uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -3861,7 +3873,7 @@ "num_bounded_loops": null }, { - "name": "check_megaMem_mload_src_ptr_reverts()", + "name": "check_concrete_keccak_does_not_split_paths()", "exitcode": 0, "num_models": 0, "models": null, @@ -3870,7 +3882,7 @@ "num_bounded_loops": null }, { - "name": "check_megaMem_mstore8_dst_ptr_reverts()", + "name": "check_concrete_keccak_memory_lookup()", "exitcode": 0, "num_models": 0, "models": null, @@ -3879,7 +3891,7 @@ "num_bounded_loops": null }, { - "name": "check_megaMem_mstore_dst_ptr_reverts()", + "name": "check_empty_hash_value()", "exitcode": 0, "num_models": 0, "models": null, @@ -3888,7 +3900,7 @@ "num_bounded_loops": null }, { - "name": "check_megaMem_new_bytes_reverts()", + "name": "check_hash()", "exitcode": 0, "num_models": 0, "models": null, @@ -3897,7 +3909,7 @@ "num_bounded_loops": null }, { - "name": "check_megaMem_return_len_reverts()", + "name": "check_hash_collision_with_empty()", "exitcode": 0, "num_models": 0, "models": null, @@ -3906,7 +3918,7 @@ "num_bounded_loops": null }, { - "name": "check_megaMem_return_ptr_reverts()", + "name": "check_no_hash_collision_assumption()", "exitcode": 0, "num_models": 0, "models": null, @@ -3915,7 +3927,7 @@ "num_bounded_loops": null }, { - "name": "check_megaMem_returndatacopy_dst_ptr_reverts(bool)", + "name": "check_only_empty_bytes_matches_empty_hash(bytes)", "exitcode": 0, "num_models": 0, "models": null, @@ -3924,16 +3936,16 @@ "num_bounded_loops": null }, { - "name": "check_megaMem_returndatacopy_src_len_reverts()", - "exitcode": 0, - "num_models": 0, + "name": "check_uint128_collision_fail(uint256,uint256)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_megaMem_returndatacopy_src_ptr_reverts()", + "name": "check_uint160_collision_pass(uint256,uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -3942,16 +3954,18 @@ "num_bounded_loops": null }, { - "name": "check_megaMem_revert_len_reverts()", + "name": "check_uint256_collision(uint256,uint256)", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/SignExtend.t.sol:SignExtendTest": [ { - "name": "check_megaMem_revert_ptr_reverts()", + "name": "check_SIGNEXTEND(int16)", "exitcode": 0, "num_models": 0, "models": null, @@ -3960,9 +3974,9 @@ "num_bounded_loops": null } ], - "test/Natspec.t.sol:NatspecTestContract": [ + "test/Signature.t.sol:SignatureTest": [ { - "name": "check_Loop3(uint256)", + "name": "check_ecrecover(bytes32,uint8,bytes32,bytes32)", "exitcode": 0, "num_models": 0, "models": null, @@ -3971,18 +3985,16 @@ "num_bounded_loops": null }, { - "name": "check_Loop3Fail(uint256)", + "name": "check_ecrecover_concrete_args()", "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)", + "name": "check_ecrecover_distinctKeysSameDigestCorrectRecovery(uint256,uint256,bytes32)", "exitcode": 0, "num_models": 0, "models": null, @@ -3991,16 +4003,16 @@ "num_bounded_loops": null }, { - "name": "check_Loop2Fail(uint256)", - "exitcode": 1, - "num_models": 1, + "name": "check_ecrecover_distinctKeysSameDigestDistinctAddrs(uint256,uint256,bytes32)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_Loop3(uint256)", + "name": "check_ecrecover_eip2098CompactSignatures(uint256,bytes32)", "exitcode": 0, "num_models": 0, "models": null, @@ -4009,18 +4021,7 @@ "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)", + "name": "check_ecrecover_explicitMalleability(uint256,bytes32)", "exitcode": 0, "num_models": 0, "models": null, @@ -4029,18 +4030,16 @@ "num_bounded_loops": null }, { - "name": "check_Loop2Fail(uint256)", - "exitcode": 1, - "num_models": 1, + "name": "check_ecrecover_sameKeyDistinctDigestsCorrectRecovery(uint256,bytes32,bytes32)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/Natspec.t.sol:NatspecTestOverwrite": [ + }, { - "name": "check_Loop3(uint256)", + "name": "check_ecrecover_sameKeyDistinctDigestsUniqueRecovery(uint256,bytes32,bytes32)", "exitcode": 0, "num_models": 0, "models": null, @@ -4049,16 +4048,16 @@ "num_bounded_loops": null }, { - "name": "check_Loop3Fail(uint256)", + "name": "check_ecrecover_solveForMalleability(uint256,bytes32)", "exitcode": 1, - "num_models": 1, + "num_models": 3, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_Loop4(uint256)", + "name": "check_isValidERC1271SignatureNow(bytes32,bytes)", "exitcode": 0, "num_models": 0, "models": null, @@ -4067,18 +4066,7 @@ "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)", + "name": "check_isValidSignatureNow(bytes32,bytes)", "exitcode": 0, "num_models": 0, "models": null, @@ -4087,18 +4075,16 @@ "num_bounded_loops": null }, { - "name": "check_Loop2Fail(uint256)", - "exitcode": 1, - "num_models": 1, + "name": "check_makeAddrAndKey_consistent_concrete()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/OpCodesBlobBaseFee_BlobHash.t.sol:BlobBaseFeeTest": [ + }, { - "name": "check_blobbasefee_affordable(uint256)", + "name": "check_makeAddrAndKey_consistent_symbolic()", "exitcode": 0, "num_models": 0, "models": null, @@ -4107,7 +4093,7 @@ "num_bounded_loops": null }, { - "name": "check_blobbasefee_no_revert()", + "name": "check_makeAddrAndKey_noCollision_concrete()", "exitcode": 0, "num_models": 0, "models": null, @@ -4116,7 +4102,7 @@ "num_bounded_loops": null }, { - "name": "check_blobbasefee_solidity_matches_opcode()", + "name": "check_makeAddrAndKey_noCollision_symbolic()", "exitcode": 0, "num_models": 0, "models": null, @@ -4125,18 +4111,16 @@ "num_bounded_loops": null }, { - "name": "check_blobbasefee_zero_in_test_env()", + "name": "check_makeAddrAndKey_vmsign_ecrecover_e2e_concrete()", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/OpCodesBlobBaseFee_BlobHash.t.sol:BlobHashTest": [ + }, { - "name": "check_blobhash_contiguous(uint8)", + "name": "check_makeAddrAndKey_vmsign_ecrecover_e2e_symbolic(string,bytes32)", "exitcode": 0, "num_models": 0, "models": null, @@ -4145,7 +4129,7 @@ "num_bounded_loops": null }, { - "name": "check_blobhash_deterministic(uint8)", + "name": "check_tryRecover(address,bytes32,bytes)", "exitcode": 0, "num_models": 0, "models": null, @@ -4154,45 +4138,43 @@ "num_bounded_loops": null }, { - "name": "check_blobhash_no_revert(uint8)", - "exitcode": 0, - "num_models": 0, + "name": "check_vmaddr_canFindKeyForAddr_concrete(uint256)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_blobhash_out_of_range()", - "exitcode": 0, - "num_models": 0, + "name": "check_vmaddr_canFindKeyForAddr_mixed(uint256)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_blobhash_version_byte(uint8)", - "exitcode": 0, - "num_models": 0, + "name": "check_vmaddr_canFindKeyForAddr_symbolic(uint256,uint256)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_blobhash_zero_no_blob_tx()", + "name": "check_vmaddr_consistent(uint256)", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/OpCodesCLZ.sol:ClzTest": [ + }, { - "name": "check_clz_byte()", + "name": "check_vmaddr_noCollision_concrete(uint256,uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -4201,7 +4183,7 @@ "num_bounded_loops": null }, { - "name": "check_clz_lower_bound(uint256)", + "name": "check_vmaddr_noCollision_symbolic(uint256,uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -4210,16 +4192,16 @@ "num_bounded_loops": null }, { - "name": "check_clz_max()", - "exitcode": 0, - "num_models": 0, + "name": "check_vmsign_canFindKeyForGivenSig(uint256,uint256,bytes32)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_clz_monotone(uint256,uint256)", + "name": "check_vmsign_consistent(uint256,bytes32)", "exitcode": 0, "num_models": 0, "models": null, @@ -4228,7 +4210,7 @@ "num_bounded_loops": null }, { - "name": "check_clz_msb()", + "name": "check_vmsign_ecrecover_e2e_recoveredCanNotMatchOtherAddr(uint256,bytes32,address)", "exitcode": 0, "num_models": 0, "models": null, @@ -4237,7 +4219,7 @@ "num_bounded_loops": null }, { - "name": "check_clz_one()", + "name": "check_vmsign_ecrecover_e2e_recoveredMatches(uint256,bytes32)", "exitcode": 0, "num_models": 0, "models": null, @@ -4246,7 +4228,7 @@ "num_bounded_loops": null }, { - "name": "check_clz_power_of_two(uint8)", + "name": "check_vmsign_noDigestCollision(uint256,bytes32,bytes32)", "exitcode": 0, "num_models": 0, "models": null, @@ -4255,7 +4237,7 @@ "num_bounded_loops": null }, { - "name": "check_clz_range(uint256)", + "name": "check_vmsign_noKeyCollision(uint256,uint256,bytes32)", "exitcode": 0, "num_models": 0, "models": null, @@ -4264,7 +4246,7 @@ "num_bounded_loops": null }, { - "name": "check_clz_two()", + "name": "check_vmsign_tryRecover(uint256,bytes32)", "exitcode": 0, "num_models": 0, "models": null, @@ -4273,45 +4255,47 @@ "num_bounded_loops": null }, { - "name": "check_clz_upper_bound(uint256)", + "name": "check_vmsign_valuesInExpectedRange(uint256,bytes32)", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/SimpleState.t.sol:SimpleStateTest": [ { - "name": "check_clz_zero()", - "exitcode": 0, - "num_models": 0, + "name": "check_buggy_excluding_view()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_clz_zero_symbolic()", - "exitcode": 0, - "num_models": 0, + "name": "check_buggy_with_state_snapshot()", + "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, + "name": "check_buggy_with_storage_snapshot()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/SmolWETH.t.sol:SmolWETHTest": [ { - "name": "check_SIGNEXTEND(uint256)", + "name": "check_deposit_once(address,uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -4320,18 +4304,9 @@ "num_bounded_loops": null } ], - "test/Panic.t.sol:PanicTest": [ - { - "name": "check_panic_0_fail(bool)", - "exitcode": 1, - "num_models": 1, - "models": null, - "num_paths": null, - "time": null, - "num_bounded_loops": null - }, + "test/Snapshot.t.sol:SnapshotTest": [ { - "name": "check_panic_0_pass(bool)", + "name": "check_balance_snapshot()", "exitcode": 0, "num_models": 0, "models": null, @@ -4340,16 +4315,16 @@ "num_bounded_loops": null }, { - "name": "check_panic_0x11_fail(uint256)", - "exitcode": 1, - "num_models": 1, + "name": "check_new_account_snapshot()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_panic_0x11_pass(uint256)", + "name": "check_snapshot()", "exitcode": 0, "num_models": 0, "models": null, @@ -4358,52 +4333,56 @@ "num_bounded_loops": null }, { - "name": "check_panic_0x12_fail(uint256)", - "exitcode": 1, - "num_models": 1, + "name": "check_this_balance_snapshot()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_panic_0x12_pass(uint256)", + "name": "check_this_storage_snapshot()", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/Solver.t.sol:SolverTest": [ { - "name": "check_panic_10_fail(bool)", - "exitcode": 1, - "num_models": 1, + "name": "check_dynamic_array_overflow()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_panic_16_fail(bool)", - "exitcode": 1, - "num_models": 1, + "name": "check_too_many_open_files()", + "exitcode": 2, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/SortMismatch.t.sol:SortMismatchTest": [ { - "name": "check_panic_1_fail(bool)", - "exitcode": 1, - "num_models": 1, + "name": "check_add(uint256)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_panic_1_pass(bool)", + "name": "check_addmod(uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -4412,70 +4391,70 @@ "num_bounded_loops": null }, { - "name": "check_panic_2_or_3_fail_2(bool)", - "exitcode": 1, - "num_models": 1, + "name": "check_and(uint256)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_panic_2_or_3_fail_3(bool)", - "exitcode": 1, - "num_models": 1, + "name": "check_div(uint256)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_panic_5_fail_old(bool)", - "exitcode": 1, - "num_models": 1, + "name": "check_eq(uint256)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_panic_7_fail(bool)", - "exitcode": 1, - "num_models": 1, + "name": "check_exp(uint256)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_panic_8_fail(bool)", - "exitcode": 1, - "num_models": 1, + "name": "check_gt(uint256)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_panic_any_fail_4(bool)", - "exitcode": 1, - "num_models": 1, + "name": "check_iszero(uint256)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_panic_inc_assert_fail(uint256)", - "exitcode": 1, - "num_models": 1, + "name": "check_lt(uint256)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_panic_inc_assert_pass(uint256)", + "name": "check_mod(uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -4484,56 +4463,52 @@ "num_bounded_loops": null }, { - "name": "check_panic_inc_cheatcode_fail(uint256)", - "exitcode": 1, - "num_models": 1, + "name": "check_mul(uint256)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_panic_inc_fail(uint256)", - "exitcode": 1, - "num_models": 1, + "name": "check_mulmod(uint256)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_panic_inc_overflow_fail(uint256)", - "exitcode": 1, - "num_models": 1, + "name": "check_not(uint256)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_panic_inc_overflow_pass(uint256)", + "name": "check_or(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)", + "name": "check_return()", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/Prank.t.sol:PrankTest": [ + }, { - "name": "check_prank_Constructor(address,address)", + "name": "check_revert()", "exitcode": 0, "num_models": 0, "models": null, @@ -4542,7 +4517,7 @@ "num_bounded_loops": null }, { - "name": "check_prank_ConstructorCreate2(address,address,bytes32)", + "name": "check_sar(uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -4551,7 +4526,7 @@ "num_bounded_loops": null }, { - "name": "check_prank_External(address)", + "name": "check_sdiv(uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -4560,7 +4535,7 @@ "num_bounded_loops": null }, { - "name": "check_prank_ExternalSelf(address)", + "name": "check_sgt(uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -4569,7 +4544,7 @@ "num_bounded_loops": null }, { - "name": "check_prank_Internal(address)", + "name": "check_shl(uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -4578,7 +4553,7 @@ "num_bounded_loops": null }, { - "name": "check_prank_New(address)", + "name": "check_shr(uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -4587,7 +4562,7 @@ "num_bounded_loops": null }, { - "name": "check_prank_Reset1(address)", + "name": "check_signextend(uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -4596,7 +4571,7 @@ "num_bounded_loops": null }, { - "name": "check_prank_Reset2(address)", + "name": "check_slt(uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -4605,7 +4580,7 @@ "num_bounded_loops": null }, { - "name": "check_prank_double(address,address)", + "name": "check_smod(uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -4614,7 +4589,7 @@ "num_bounded_loops": null }, { - "name": "check_prank_single(address)", + "name": "check_sub(uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -4623,16 +4598,18 @@ "num_bounded_loops": null }, { - "name": "check_prank_startPrank_in_constructor(address,address)", + "name": "check_xor(uint256)", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/StaticContexts.t.sol:StaticContextsTest": [ { - "name": "check_startPrank_double(address,address)", + "name": "check_create2_fails()", "exitcode": 0, "num_models": 0, "models": null, @@ -4641,7 +4618,7 @@ "num_bounded_loops": null }, { - "name": "check_startPrank_single(address)", + "name": "check_create_fails()", "exitcode": 0, "num_models": 0, "models": null, @@ -4650,7 +4627,7 @@ "num_bounded_loops": null }, { - "name": "check_stopPrank_1(address)", + "name": "check_log_fails()", "exitcode": 0, "num_models": 0, "models": null, @@ -4659,7 +4636,7 @@ "num_bounded_loops": null }, { - "name": "check_stopPrank_2()", + "name": "check_sstore_fails()", "exitcode": 0, "num_models": 0, "models": null, @@ -4668,29 +4645,27 @@ "num_bounded_loops": null } ], - "test/Proxy.t.sol:ProxyTest": [ + "test/StdAssertTest.t.sol:StdAssertFailLogTest": [ { - "name": "check_foo(uint256,uint256,address)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertEq(address,address)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/Revert.t.sol:CTest": [ + }, { - "name": "check_BalanceTransfer_Known(uint256,uint256)", + "name": "check_assertEq(address[],address[])", "exitcode": 1, - "num_models": 1, + "num_models": 8, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_BalanceTransfer_Unknown(uint256,uint256)", + "name": "check_assertEq(bool,bool)", "exitcode": 1, "num_models": 1, "models": null, @@ -4699,200 +4674,187 @@ "num_bounded_loops": null }, { - "name": "check_Revert1(uint256)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertEq(bool[],bool[])", + "exitcode": 1, + "num_models": 8, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_Revert2(uint256)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertEq(bytes,bytes)", + "exitcode": 1, + "num_models": 8, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_RevertBalance_Known(bool,uint256)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertEq(bytes32,bytes32)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_RevertBalance_Unknown(uint256,uint256)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertEq(bytes32[],bytes32[])", + "exitcode": 1, + "num_models": 8, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_RevertCode(address)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertEq(int256,int256)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/Send.t.sol:SendTest": [ + }, { - "name": "check_create(uint256,bytes32,uint256)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertEq(int256[],int256[])", + "exitcode": 1, + "num_models": 8, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_send(uint256,uint256)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertEq(string,string)", + "exitcode": 1, + "num_models": 8, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_transfer(uint256)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertEq(uint256,uint256)", + "exitcode": 1, + "num_models": 1, "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, + "name": "check_assertEq(uint256[],uint256[])", + "exitcode": 1, + "num_models": 8, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/SetupPlus.t.sol:SetupPlusTest": [ + }, { - "name": "check_Setup()", - "exitcode": 0, - "num_models": 0, + "name": "check_assertFalse(bool)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/SetupPlus.t.sol:SetupPlusTestB": [ + }, { - "name": "check_Setup()", - "exitcode": 0, - "num_models": 0, + "name": "check_assertGe(int256,int256)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/SetupSymbolic.t.sol:SetupSymbolicTest": [ + }, { - "name": "check_True()", - "exitcode": 0, - "num_models": 0, + "name": "check_assertGe(uint256,uint256)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/Sha3.t.sol:Sha3Test": [ + }, { - "name": "check_address_collision_pass(uint256,uint256)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertGt(int256,int256)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_concrete_keccak_does_not_split_paths()", - "exitcode": 0, - "num_models": 0, + "name": "check_assertGt(uint256,uint256)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_concrete_keccak_memory_lookup()", - "exitcode": 0, - "num_models": 0, + "name": "check_assertLe(int256,int256)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_empty_hash_value()", - "exitcode": 0, - "num_models": 0, + "name": "check_assertLe(uint256,uint256)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_hash()", - "exitcode": 0, - "num_models": 0, + "name": "check_assertLt(int256,int256)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_hash_collision_with_empty()", - "exitcode": 0, - "num_models": 0, + "name": "check_assertLt(uint256,uint256)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_no_hash_collision_assumption()", - "exitcode": 0, - "num_models": 0, + "name": "check_assertNotEq(address,address)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_only_empty_bytes_matches_empty_hash(bytes)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertNotEq(address[],address[])", + "exitcode": 1, + "num_models": 3, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_uint128_collision_fail(uint256,uint256)", + "name": "check_assertNotEq(bool,bool)", "exitcode": 1, "num_models": 1, "models": null, @@ -4901,47 +4863,117 @@ "num_bounded_loops": null }, { - "name": "check_uint160_collision_pass(uint256,uint256)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertNotEq(bool[],bool[])", + "exitcode": 1, + "num_models": 3, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_uint256_collision(uint256,uint256)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertNotEq(bytes,bytes)", + "exitcode": 1, + "num_models": 3, "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, + "name": "check_assertNotEq(bytes32,bytes32)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_assertNotEq(bytes32[],bytes32[])", + "exitcode": 1, + "num_models": 3, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_assertNotEq(int256,int256)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_assertNotEq(int256[],int256[])", + "exitcode": 1, + "num_models": 3, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_assertNotEq(string,string)", + "exitcode": 1, + "num_models": 3, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_assertNotEq(uint256,uint256)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_assertNotEq(uint256[],uint256[])", + "exitcode": 1, + "num_models": 3, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_assertTrue(bool)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null } ], - "test/Signature.t.sol:SignatureTest": [ + "test/StdAssertTest.t.sol:StdAssertFailTest": [ { - "name": "check_ecrecover(bytes32,uint8,bytes32,bytes32)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertEq(address,address)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_ecrecover_concrete_args()", + "name": "check_assertEq(address[],address[])", + "exitcode": 1, + "num_models": 8, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_assertEq(bool,bool)", "exitcode": 1, "num_models": 1, "models": null, @@ -4950,151 +4982,151 @@ "num_bounded_loops": null }, { - "name": "check_ecrecover_distinctKeysSameDigestCorrectRecovery(uint256,uint256,bytes32)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertEq(bool[],bool[])", + "exitcode": 1, + "num_models": 8, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_ecrecover_distinctKeysSameDigestDistinctAddrs(uint256,uint256,bytes32)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertEq(bytes,bytes)", + "exitcode": 1, + "num_models": 8, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_ecrecover_eip2098CompactSignatures(uint256,bytes32)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertEq(bytes32,bytes32)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_ecrecover_explicitMalleability(uint256,bytes32)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertEq(bytes32[],bytes32[])", + "exitcode": 1, + "num_models": 8, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_ecrecover_sameKeyDistinctDigestsCorrectRecovery(uint256,bytes32,bytes32)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertEq(int256,int256)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_ecrecover_sameKeyDistinctDigestsUniqueRecovery(uint256,bytes32,bytes32)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertEq(int256[],int256[])", + "exitcode": 1, + "num_models": 8, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_ecrecover_solveForMalleability(uint256,bytes32)", + "name": "check_assertEq(string,string)", "exitcode": 1, - "num_models": 3, + "num_models": 8, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_isValidERC1271SignatureNow(bytes32,bytes)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertEq(uint256,uint256)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_isValidSignatureNow(bytes32,bytes)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertEq(uint256[],uint256[])", + "exitcode": 1, + "num_models": 8, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_makeAddrAndKey_consistent_concrete()", - "exitcode": 0, - "num_models": 0, + "name": "check_assertFalse(bool)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_makeAddrAndKey_consistent_symbolic()", - "exitcode": 0, - "num_models": 0, + "name": "check_assertGe(int256,int256)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_makeAddrAndKey_noCollision_concrete()", - "exitcode": 0, - "num_models": 0, + "name": "check_assertGe(uint256,uint256)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_makeAddrAndKey_noCollision_symbolic()", - "exitcode": 0, - "num_models": 0, + "name": "check_assertGt(int256,int256)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_makeAddrAndKey_vmsign_ecrecover_e2e_concrete()", - "exitcode": 0, - "num_models": 0, + "name": "check_assertGt(uint256,uint256)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_makeAddrAndKey_vmsign_ecrecover_e2e_symbolic(string,bytes32)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertLe(int256,int256)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_tryRecover(address,bytes32,bytes)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertLe(uint256,uint256)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_vmaddr_canFindKeyForAddr_concrete(uint256)", + "name": "check_assertLt(int256,int256)", "exitcode": 1, "num_models": 1, "models": null, @@ -5103,7 +5135,7 @@ "num_bounded_loops": null }, { - "name": "check_vmaddr_canFindKeyForAddr_mixed(uint256)", + "name": "check_assertLt(uint256,uint256)", "exitcode": 1, "num_models": 1, "models": null, @@ -5112,7 +5144,7 @@ "num_bounded_loops": null }, { - "name": "check_vmaddr_canFindKeyForAddr_symbolic(uint256,uint256)", + "name": "check_assertNotEq(address,address)", "exitcode": 1, "num_models": 1, "models": null, @@ -5121,34 +5153,43 @@ "num_bounded_loops": null }, { - "name": "check_vmaddr_consistent(uint256)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertNotEq(address[],address[])", + "exitcode": 1, + "num_models": 3, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_vmaddr_noCollision_concrete(uint256,uint256)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertNotEq(bool,bool)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_vmaddr_noCollision_symbolic(uint256,uint256)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertNotEq(bool[],bool[])", + "exitcode": 1, + "num_models": 3, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_vmsign_canFindKeyForGivenSig(uint256,uint256,bytes32)", + "name": "check_assertNotEq(bytes,bytes)", + "exitcode": 1, + "num_models": 3, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_assertNotEq(bytes32,bytes32)", "exitcode": 1, "num_models": 1, "models": null, @@ -5157,112 +5198,108 @@ "num_bounded_loops": null }, { - "name": "check_vmsign_consistent(uint256,bytes32)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertNotEq(bytes32[],bytes32[])", + "exitcode": 1, + "num_models": 3, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_vmsign_ecrecover_e2e_recoveredCanNotMatchOtherAddr(uint256,bytes32,address)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertNotEq(int256,int256)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_vmsign_ecrecover_e2e_recoveredMatches(uint256,bytes32)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertNotEq(int256[],int256[])", + "exitcode": 1, + "num_models": 3, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_vmsign_noDigestCollision(uint256,bytes32,bytes32)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertNotEq(string,string)", + "exitcode": 1, + "num_models": 3, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_vmsign_noKeyCollision(uint256,uint256,bytes32)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertNotEq(uint256,uint256)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_vmsign_tryRecover(uint256,bytes32)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertNotEq(uint256[],uint256[])", + "exitcode": 1, + "num_models": 3, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_vmsign_valuesInExpectedRange(uint256,bytes32)", - "exitcode": 0, - "num_models": 0, + "name": "check_assertTrue(bool)", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null } ], - "test/SimpleState.t.sol:SimpleStateTest": [ + "test/StdAssertTest.t.sol:StdAssertPassTest": [ { - "name": "check_buggy_excluding_view()", - "exitcode": 1, - "num_models": 1, + "name": "check_assertEq(address,address)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_buggy_with_state_snapshot()", - "exitcode": 1, - "num_models": 1, + "name": "check_assertEq(address[],address[])", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_buggy_with_storage_snapshot()", - "exitcode": 1, - "num_models": 1, + "name": "check_assertEq(bool,bool)", + "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)", + "name": "check_assertEq(bool[],bool[])", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/Snapshot.t.sol:SnapshotTest": [ + }, { - "name": "check_balance_snapshot()", + "name": "check_assertEq(bytes,bytes)", "exitcode": 0, "num_models": 0, "models": null, @@ -5271,7 +5308,7 @@ "num_bounded_loops": null }, { - "name": "check_new_account_snapshot()", + "name": "check_assertEq(bytes32,bytes32)", "exitcode": 0, "num_models": 0, "models": null, @@ -5280,7 +5317,7 @@ "num_bounded_loops": null }, { - "name": "check_snapshot()", + "name": "check_assertEq(bytes32[],bytes32[])", "exitcode": 0, "num_models": 0, "models": null, @@ -5289,7 +5326,7 @@ "num_bounded_loops": null }, { - "name": "check_this_balance_snapshot()", + "name": "check_assertEq(int256,int256)", "exitcode": 0, "num_models": 0, "models": null, @@ -5298,18 +5335,16 @@ "num_bounded_loops": null }, { - "name": "check_this_storage_snapshot()", + "name": "check_assertEq(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_dynamic_array_overflow()", + "name": "check_assertEq(string,string)", "exitcode": 0, "num_models": 0, "models": null, @@ -5318,18 +5353,16 @@ "num_bounded_loops": null }, { - "name": "check_too_many_open_files()", - "exitcode": 2, + "name": "check_assertEq(uint256,uint256)", + "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/SortMismatch.t.sol:SortMismatchTest": [ + }, { - "name": "check_add(uint256)", + "name": "check_assertEq(uint256[],uint256[])", "exitcode": 0, "num_models": 0, "models": null, @@ -5338,7 +5371,7 @@ "num_bounded_loops": null }, { - "name": "check_addmod(uint256)", + "name": "check_assertFalse(bool)", "exitcode": 0, "num_models": 0, "models": null, @@ -5347,7 +5380,7 @@ "num_bounded_loops": null }, { - "name": "check_and(uint256)", + "name": "check_assertGe(int256,int256)", "exitcode": 0, "num_models": 0, "models": null, @@ -5356,7 +5389,7 @@ "num_bounded_loops": null }, { - "name": "check_div(uint256)", + "name": "check_assertGe(uint256,uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -5365,7 +5398,7 @@ "num_bounded_loops": null }, { - "name": "check_eq(uint256)", + "name": "check_assertGt(int256,int256)", "exitcode": 0, "num_models": 0, "models": null, @@ -5374,7 +5407,7 @@ "num_bounded_loops": null }, { - "name": "check_exp(uint256)", + "name": "check_assertGt(uint256,uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -5383,7 +5416,7 @@ "num_bounded_loops": null }, { - "name": "check_gt(uint256)", + "name": "check_assertLe(int256,int256)", "exitcode": 0, "num_models": 0, "models": null, @@ -5392,7 +5425,7 @@ "num_bounded_loops": null }, { - "name": "check_iszero(uint256)", + "name": "check_assertLe(uint256,uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -5401,7 +5434,7 @@ "num_bounded_loops": null }, { - "name": "check_lt(uint256)", + "name": "check_assertLt(int256,int256)", "exitcode": 0, "num_models": 0, "models": null, @@ -5410,7 +5443,7 @@ "num_bounded_loops": null }, { - "name": "check_mod(uint256)", + "name": "check_assertLt(uint256,uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -5419,7 +5452,7 @@ "num_bounded_loops": null }, { - "name": "check_mul(uint256)", + "name": "check_assertNotEq(address,address)", "exitcode": 0, "num_models": 0, "models": null, @@ -5428,7 +5461,7 @@ "num_bounded_loops": null }, { - "name": "check_mulmod(uint256)", + "name": "check_assertNotEq(address[],address[])", "exitcode": 0, "num_models": 0, "models": null, @@ -5437,7 +5470,7 @@ "num_bounded_loops": null }, { - "name": "check_not(uint256)", + "name": "check_assertNotEq(bool,bool)", "exitcode": 0, "num_models": 0, "models": null, @@ -5446,7 +5479,7 @@ "num_bounded_loops": null }, { - "name": "check_or(uint256)", + "name": "check_assertNotEq(bool[],bool[])", "exitcode": 0, "num_models": 0, "models": null, @@ -5455,7 +5488,7 @@ "num_bounded_loops": null }, { - "name": "check_return()", + "name": "check_assertNotEq(bytes,bytes)", "exitcode": 0, "num_models": 0, "models": null, @@ -5464,7 +5497,7 @@ "num_bounded_loops": null }, { - "name": "check_revert()", + "name": "check_assertNotEq(bytes32,bytes32)", "exitcode": 0, "num_models": 0, "models": null, @@ -5473,7 +5506,7 @@ "num_bounded_loops": null }, { - "name": "check_sar(uint256)", + "name": "check_assertNotEq(bytes32[],bytes32[])", "exitcode": 0, "num_models": 0, "models": null, @@ -5482,7 +5515,7 @@ "num_bounded_loops": null }, { - "name": "check_sdiv(uint256)", + "name": "check_assertNotEq(int256,int256)", "exitcode": 0, "num_models": 0, "models": null, @@ -5491,7 +5524,7 @@ "num_bounded_loops": null }, { - "name": "check_sgt(uint256)", + "name": "check_assertNotEq(int256[],int256[])", "exitcode": 0, "num_models": 0, "models": null, @@ -5500,7 +5533,7 @@ "num_bounded_loops": null }, { - "name": "check_shl(uint256)", + "name": "check_assertNotEq(string,string)", "exitcode": 0, "num_models": 0, "models": null, @@ -5509,7 +5542,7 @@ "num_bounded_loops": null }, { - "name": "check_shr(uint256)", + "name": "check_assertNotEq(uint256,uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -5518,7 +5551,7 @@ "num_bounded_loops": null }, { - "name": "check_signextend(uint256)", + "name": "check_assertNotEq(uint256[],uint256[])", "exitcode": 0, "num_models": 0, "models": null, @@ -5527,16 +5560,18 @@ "num_bounded_loops": null }, { - "name": "check_slt(uint256)", + "name": "check_assertTrue(bool)", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/Storage.t.sol:StorageTest": [ { - "name": "check_smod(uint256)", + "name": "check_addArr1(uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -5545,7 +5580,7 @@ "num_bounded_loops": null }, { - "name": "check_sub(uint256)", + "name": "check_addArr2(uint256,uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -5554,18 +5589,16 @@ "num_bounded_loops": null }, { - "name": "check_xor(uint256)", + "name": "check_addMap1Arr1(uint256,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()", + "name": "check_setMap1(uint256,uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -5574,7 +5607,7 @@ "num_bounded_loops": null }, { - "name": "check_create_fails()", + "name": "check_setMap2(uint256,uint256,uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -5583,16 +5616,18 @@ "num_bounded_loops": null }, { - "name": "check_log_fails()", + "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_sstore_fails()", + "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, @@ -5601,54 +5636,60 @@ "num_bounded_loops": null } ], - "test/StdAssertTest.t.sol:StdAssertFailLogTest": [ + "test/Storage3.t.sol:Storage3Test": [ { - "name": "check_assertEq(address,address)", - "exitcode": 1, - "num_models": 1, + "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_assertEq(address[],address[])", - "exitcode": 1, - "num_models": 8, + "name": "check_add_1(uint256)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertEq(bool,bool)", - "exitcode": 1, - "num_models": 1, + "name": "check_add_2(uint256)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/Storage5.t.sol:Storage5Test": [ { - "name": "check_assertEq(bool[],bool[])", - "exitcode": 1, - "num_models": 8, + "name": "check_array_access(uint256)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/StorageSlot.t.sol:StorageSlotTest": [ { - "name": "check_assertEq(bytes,bytes)", - "exitcode": 1, - "num_models": 8, + "name": "check_keccak_slot_1_pass(uint256)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertEq(bytes32,bytes32)", + "name": "check_keccak_slot_2_fail(uint256)", "exitcode": 1, "num_models": 1, "models": null, @@ -5657,79 +5698,83 @@ "num_bounded_loops": null }, { - "name": "check_assertEq(bytes32[],bytes32[])", - "exitcode": 1, - "num_models": 8, + "name": "check_keccak_slot_2_pass(uint256)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertEq(int256,int256)", - "exitcode": 1, - "num_models": 1, + "name": "check_keccak_slot_3_pass(uint256)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertEq(int256[],int256[])", - "exitcode": 1, - "num_models": 8, + "name": "check_setup()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/Store.t.sol:StoreTest": [ { - "name": "check_assertEq(string,string)", - "exitcode": 1, - "num_models": 8, + "name": "check_load_nonexistent_account(uint256)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertEq(uint256,uint256)", - "exitcode": 1, - "num_models": 1, + "name": "check_store_Array(uint256,uint256)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertEq(uint256[],uint256[])", - "exitcode": 1, - "num_models": 8, + "name": "check_store_Mapping(uint256,uint256)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertFalse(bool)", - "exitcode": 1, - "num_models": 1, + "name": "check_store_Scalar(uint256)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertGe(int256,int256)", - "exitcode": 1, - "num_models": 1, + "name": "check_store_nonexistent_account()", + "exitcode": 3, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/Struct.t.sol:StructTest": [ { - "name": "check_assertGe(uint256,uint256)", + "name": "check_Struct((uint256,uint256))", "exitcode": 1, "num_models": 1, "models": null, @@ -5738,7 +5783,7 @@ "num_bounded_loops": null }, { - "name": "check_assertGt(int256,int256)", + "name": "check_StructArray((uint256,uint256)[],(uint256,uint256)[2])", "exitcode": 1, "num_models": 1, "models": null, @@ -5747,479 +5792,479 @@ "num_bounded_loops": null }, { - "name": "check_assertGt(uint256,uint256)", + "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_assertLe(int256,int256)", + "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/SvmCheatCode.t.sol:SvmCheatCodeTest": [ { - "name": "check_assertLe(uint256,uint256)", - "exitcode": 1, - "num_models": 1, + "name": "check_FailUnknownCheatcode()", + "exitcode": 3, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertLt(int256,int256)", - "exitcode": 1, - "num_models": 1, + "name": "check_SymbolLabel()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertLt(uint256,uint256)", - "exitcode": 1, - "num_models": 1, + "name": "check_createAddress()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertNotEq(address,address)", - "exitcode": 1, - "num_models": 1, + "name": "check_createBool()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertNotEq(address[],address[])", - "exitcode": 1, - "num_models": 3, + "name": "check_createBytes()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertNotEq(bool,bool)", - "exitcode": 1, - "num_models": 1, + "name": "check_createBytes32()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertNotEq(bool[],bool[])", - "exitcode": 1, - "num_models": 3, + "name": "check_createBytes4_basic()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertNotEq(bytes,bytes)", + "name": "check_createBytes4_finds_selector()", "exitcode": 1, - "num_models": 3, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertNotEq(bytes32,bytes32)", - "exitcode": 1, - "num_models": 1, + "name": "check_createBytes_empty()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertNotEq(bytes32[],bytes32[])", - "exitcode": 1, - "num_models": 3, + "name": "check_createInt()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertNotEq(int256,int256)", - "exitcode": 1, - "num_models": 1, + "name": "check_createInt256()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertNotEq(int256[],int256[])", - "exitcode": 1, - "num_models": 3, + "name": "check_createString()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertNotEq(string,string)", - "exitcode": 1, - "num_models": 3, + "name": "check_createString_empty()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertNotEq(uint256,uint256)", - "exitcode": 1, - "num_models": 1, + "name": "check_createUint()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertNotEq(uint256[],uint256[])", - "exitcode": 1, - "num_models": 3, + "name": "check_createUint256()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertTrue(bool)", + "name": "check_enableSymbolicStorage_fail()", "exitcode": 1, "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/StdAssertTest.t.sol:StdAssertFailTest": [ + }, { - "name": "check_assertEq(address,address)", - "exitcode": 1, - "num_models": 1, + "name": "check_enableSymbolicStorage_nonexistent()", + "exitcode": 3, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertEq(address[],address[])", - "exitcode": 1, - "num_models": 8, + "name": "check_enableSymbolicStorage_pass(uint256)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertEq(bool,bool)", - "exitcode": 1, - "num_models": 1, + "name": "check_env_address()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertEq(bool[],bool[])", - "exitcode": 1, - "num_models": 8, + "name": "check_env_address_array()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertEq(bytes,bytes)", - "exitcode": 1, - "num_models": 8, + "name": "check_env_bool()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertEq(bytes32,bytes32)", - "exitcode": 1, - "num_models": 1, + "name": "check_env_bool_array()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertEq(bytes32[],bytes32[])", - "exitcode": 1, - "num_models": 8, + "name": "check_env_bytes()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertEq(int256,int256)", - "exitcode": 1, - "num_models": 1, + "name": "check_env_bytes32()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertEq(int256[],int256[])", - "exitcode": 1, - "num_models": 8, + "name": "check_env_bytes32_array()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertEq(string,string)", - "exitcode": 1, - "num_models": 8, + "name": "check_env_bytes_array()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertEq(uint256,uint256)", - "exitcode": 1, - "num_models": 1, + "name": "check_env_exists()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertEq(uint256[],uint256[])", - "exitcode": 1, - "num_models": 8, + "name": "check_env_int()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertFalse(bool)", - "exitcode": 1, - "num_models": 1, + "name": "check_env_int_array()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertGe(int256,int256)", - "exitcode": 1, - "num_models": 1, + "name": "check_env_missing_fail()", + "exitcode": 5, + "num_models": null, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertGe(uint256,uint256)", - "exitcode": 1, - "num_models": 1, + "name": "check_env_or_address()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertGt(int256,int256)", - "exitcode": 1, - "num_models": 1, + "name": "check_env_or_address_array()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertGt(uint256,uint256)", - "exitcode": 1, - "num_models": 1, + "name": "check_env_or_address_array_without_env_var()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertLe(int256,int256)", - "exitcode": 1, - "num_models": 1, + "name": "check_env_or_address_without_env_var()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertLe(uint256,uint256)", - "exitcode": 1, - "num_models": 1, + "name": "check_env_or_bool()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertLt(int256,int256)", - "exitcode": 1, - "num_models": 1, + "name": "check_env_or_bool_array()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertLt(uint256,uint256)", - "exitcode": 1, - "num_models": 1, + "name": "check_env_or_bool_array_without_env_var()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertNotEq(address,address)", - "exitcode": 1, - "num_models": 1, + "name": "check_env_or_bool_without_env_var(bool)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertNotEq(address[],address[])", - "exitcode": 1, - "num_models": 3, + "name": "check_env_or_bytes()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertNotEq(bool,bool)", - "exitcode": 1, - "num_models": 1, + "name": "check_env_or_bytes32()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertNotEq(bool[],bool[])", - "exitcode": 1, - "num_models": 3, + "name": "check_env_or_bytes32_array()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertNotEq(bytes,bytes)", - "exitcode": 1, - "num_models": 3, + "name": "check_env_or_bytes32_array_without_env_var()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertNotEq(bytes32,bytes32)", - "exitcode": 1, - "num_models": 1, + "name": "check_env_or_bytes32_without_env_var()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertNotEq(bytes32[],bytes32[])", - "exitcode": 1, - "num_models": 3, + "name": "check_env_or_bytes_array()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertNotEq(int256,int256)", - "exitcode": 1, - "num_models": 1, + "name": "check_env_or_bytes_array_without_env_var()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertNotEq(int256[],int256[])", - "exitcode": 1, - "num_models": 3, + "name": "check_env_or_bytes_without_env_var()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertNotEq(string,string)", - "exitcode": 1, - "num_models": 3, + "name": "check_env_or_int(int256)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertNotEq(uint256,uint256)", - "exitcode": 1, - "num_models": 1, + "name": "check_env_or_int_array()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertNotEq(uint256[],uint256[])", - "exitcode": 1, - "num_models": 3, + "name": "check_env_or_int_array_without_env_var()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertTrue(bool)", - "exitcode": 1, - "num_models": 1, + "name": "check_env_or_string()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/StdAssertTest.t.sol:StdAssertPassTest": [ + }, { - "name": "check_assertEq(address,address)", + "name": "check_env_or_string_array()", "exitcode": 0, "num_models": 0, "models": null, @@ -6228,7 +6273,7 @@ "num_bounded_loops": null }, { - "name": "check_assertEq(address[],address[])", + "name": "check_env_or_string_array_without_env_var()", "exitcode": 0, "num_models": 0, "models": null, @@ -6237,7 +6282,7 @@ "num_bounded_loops": null }, { - "name": "check_assertEq(bool,bool)", + "name": "check_env_or_string_without_env_var()", "exitcode": 0, "num_models": 0, "models": null, @@ -6246,7 +6291,7 @@ "num_bounded_loops": null }, { - "name": "check_assertEq(bool[],bool[])", + "name": "check_env_or_uint(uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -6255,7 +6300,7 @@ "num_bounded_loops": null }, { - "name": "check_assertEq(bytes,bytes)", + "name": "check_env_or_uint_array()", "exitcode": 0, "num_models": 0, "models": null, @@ -6264,7 +6309,7 @@ "num_bounded_loops": null }, { - "name": "check_assertEq(bytes32,bytes32)", + "name": "check_env_or_uint_array_without_env_var()", "exitcode": 0, "num_models": 0, "models": null, @@ -6273,7 +6318,7 @@ "num_bounded_loops": null }, { - "name": "check_assertEq(bytes32[],bytes32[])", + "name": "check_env_string()", "exitcode": 0, "num_models": 0, "models": null, @@ -6282,7 +6327,7 @@ "num_bounded_loops": null }, { - "name": "check_assertEq(int256,int256)", + "name": "check_env_string_array()", "exitcode": 0, "num_models": 0, "models": null, @@ -6291,7 +6336,7 @@ "num_bounded_loops": null }, { - "name": "check_assertEq(int256[],int256[])", + "name": "check_env_uint()", "exitcode": 0, "num_models": 0, "models": null, @@ -6300,7 +6345,7 @@ "num_bounded_loops": null }, { - "name": "check_assertEq(string,string)", + "name": "check_env_uint_array()", "exitcode": 0, "num_models": 0, "models": null, @@ -6309,8 +6354,8 @@ "num_bounded_loops": null }, { - "name": "check_assertEq(uint256,uint256)", - "exitcode": 0, + "name": "check_extract_string_argument_fail(string)", + "exitcode": 3, "num_models": 0, "models": null, "num_paths": null, @@ -6318,7 +6363,7 @@ "num_bounded_loops": null }, { - "name": "check_assertEq(uint256[],uint256[])", + "name": "check_randomAddress()", "exitcode": 0, "num_models": 0, "models": null, @@ -6327,7 +6372,7 @@ "num_bounded_loops": null }, { - "name": "check_assertFalse(bool)", + "name": "check_randomBool()", "exitcode": 0, "num_models": 0, "models": null, @@ -6336,7 +6381,7 @@ "num_bounded_loops": null }, { - "name": "check_assertGe(int256,int256)", + "name": "check_randomBytes()", "exitcode": 0, "num_models": 0, "models": null, @@ -6345,7 +6390,7 @@ "num_bounded_loops": null }, { - "name": "check_assertGe(uint256,uint256)", + "name": "check_randomBytes4()", "exitcode": 0, "num_models": 0, "models": null, @@ -6354,7 +6399,7 @@ "num_bounded_loops": null }, { - "name": "check_assertGt(int256,int256)", + "name": "check_randomBytes8()", "exitcode": 0, "num_models": 0, "models": null, @@ -6363,7 +6408,7 @@ "num_bounded_loops": null }, { - "name": "check_assertGt(uint256,uint256)", + "name": "check_randomBytes_empty()", "exitcode": 0, "num_models": 0, "models": null, @@ -6372,7 +6417,7 @@ "num_bounded_loops": null }, { - "name": "check_assertLe(int256,int256)", + "name": "check_randomInt()", "exitcode": 0, "num_models": 0, "models": null, @@ -6381,7 +6426,7 @@ "num_bounded_loops": null }, { - "name": "check_assertLe(uint256,uint256)", + "name": "check_randomInt256()", "exitcode": 0, "num_models": 0, "models": null, @@ -6390,7 +6435,7 @@ "num_bounded_loops": null }, { - "name": "check_assertLt(int256,int256)", + "name": "check_randomUint()", "exitcode": 0, "num_models": 0, "models": null, @@ -6399,7 +6444,7 @@ "num_bounded_loops": null }, { - "name": "check_assertLt(uint256,uint256)", + "name": "check_randomUint256()", "exitcode": 0, "num_models": 0, "models": null, @@ -6408,7 +6453,7 @@ "num_bounded_loops": null }, { - "name": "check_assertNotEq(address,address)", + "name": "check_random_uint_range_concrete_pass()", "exitcode": 0, "num_models": 0, "models": null, @@ -6417,8 +6462,8 @@ "num_bounded_loops": null }, { - "name": "check_assertNotEq(address[],address[])", - "exitcode": 0, + "name": "check_random_uint_range_max_greaterthan_min_fail()", + "exitcode": 3, "num_models": 0, "models": null, "num_paths": null, @@ -6426,8 +6471,8 @@ "num_bounded_loops": null }, { - "name": "check_assertNotEq(bool,bool)", - "exitcode": 0, + "name": "check_random_uint_range_symbolic_fail(uint256,uint256)", + "exitcode": 3, "num_models": 0, "models": null, "num_paths": null, @@ -6435,17 +6480,17 @@ "num_bounded_loops": null }, { - "name": "check_assertNotEq(bool[],bool[])", - "exitcode": 0, - "num_models": 0, + "name": "check_setArbitraryStorage_fail()", + "exitcode": 1, + "num_models": 1, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_assertNotEq(bytes,bytes)", - "exitcode": 0, + "name": "check_setArbitraryStorage_nonexistent()", + "exitcode": 3, "num_models": 0, "models": null, "num_paths": null, @@ -6453,7 +6498,7 @@ "num_bounded_loops": null }, { - "name": "check_assertNotEq(bytes32,bytes32)", + "name": "check_setArbitraryStorage_pass(uint256)", "exitcode": 0, "num_models": 0, "models": null, @@ -6462,17 +6507,19 @@ "num_bounded_loops": null }, { - "name": "check_assertNotEq(bytes32[],bytes32[])", - "exitcode": 0, + "name": "check_unsupported_cheatcode_fail()", + "exitcode": 3, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - }, + } + ], + "test/SvmCheatCode.t.sol:SvmCreateCalldataTest": [ { - "name": "check_assertNotEq(int256,int256)", - "exitcode": 0, + "name": "check_createCalldata_Beep_0_excluding_pure()", + "exitcode": 4, "num_models": 0, "models": null, "num_paths": null, @@ -6480,7 +6527,7 @@ "num_bounded_loops": null }, { - "name": "check_assertNotEq(int256[],int256[])", + "name": "check_createCalldata_Beep_0_including_pure()", "exitcode": 0, "num_models": 0, "models": null, @@ -6489,8 +6536,8 @@ "num_bounded_loops": null }, { - "name": "check_assertNotEq(string,string)", - "exitcode": 0, + "name": "check_createCalldata_Beep_1_excluding_pure()", + "exitcode": 4, "num_models": 0, "models": null, "num_paths": null, @@ -6498,7 +6545,7 @@ "num_bounded_loops": null }, { - "name": "check_assertNotEq(uint256,uint256)", + "name": "check_createCalldata_Beep_1_including_pure()", "exitcode": 0, "num_models": 0, "models": null, @@ -6507,8 +6554,8 @@ "num_bounded_loops": null }, { - "name": "check_assertNotEq(uint256[],uint256[])", - "exitcode": 0, + "name": "check_createCalldata_Beep_2_excluding_pure()", + "exitcode": 4, "num_models": 0, "models": null, "num_paths": null, @@ -6516,19 +6563,17 @@ "num_bounded_loops": null }, { - "name": "check_assertTrue(bool)", + "name": "check_createCalldata_Beep_2_including_pure()", "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, + "name": "check_createCalldata_Dummy_fail()", + "exitcode": 3, "num_models": 0, "models": null, "num_paths": null, @@ -6536,7 +6581,7 @@ "num_bounded_loops": null }, { - "name": "check_addArr2(uint256,uint256)", + "name": "check_createCalldata_Fallback()", "exitcode": 0, "num_models": 0, "models": null, @@ -6545,16 +6590,16 @@ "num_bounded_loops": null }, { - "name": "check_addMap1Arr1(uint256,uint256)", - "exitcode": 0, - "num_models": 0, + "name": "check_createCalldata_Mock_0_excluding_view_fail()", + "exitcode": 1, + "num_models": 18, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_setMap1(uint256,uint256)", + "name": "check_createCalldata_Mock_0_excluding_view_pass()", "exitcode": 0, "num_models": 0, "models": null, @@ -6563,80 +6608,70 @@ "num_bounded_loops": null }, { - "name": "check_setMap2(uint256,uint256,uint256)", - "exitcode": 0, - "num_models": 0, + "name": "check_createCalldata_Mock_0_including_view_fail()", + "exitcode": 1, + "num_models": 20, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_setMap3(uint256,uint256,uint256,uint256)", + "name": "check_createCalldata_Mock_0_including_view_pass()", "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, + "name": "check_createCalldata_Mock_1_fail()", + "exitcode": 1, + "num_models": 18, "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))", + "name": "check_createCalldata_Mock_1_pass()", "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, + "name": "check_createCalldata_Mock_2_excluding_view_fail()", + "exitcode": 1, + "num_models": 18, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_add_2(uint256)", + "name": "check_createCalldata_Mock_2_excluding_view_pass()", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/Storage5.t.sol:Storage5Test": [ + }, { - "name": "check_array_access(uint256)", - "exitcode": 0, - "num_models": 0, + "name": "check_createCalldata_Mock_2_including_view_fail()", + "exitcode": 1, + "num_models": 20, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/StorageSlot.t.sol:StorageSlotTest": [ + }, { - "name": "check_keccak_slot_1_pass(uint256)", + "name": "check_createCalldata_Mock_2_including_view_pass()", "exitcode": 0, "num_models": 0, "models": null, @@ -6645,16 +6680,16 @@ "num_bounded_loops": null }, { - "name": "check_keccak_slot_2_fail(uint256)", + "name": "check_createCalldata_Mock_3_fail()", "exitcode": 1, - "num_models": 1, + "num_models": 18, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_keccak_slot_2_pass(uint256)", + "name": "check_createCalldata_Mock_3_pass()", "exitcode": 0, "num_models": 0, "models": null, @@ -6663,36 +6698,34 @@ "num_bounded_loops": null }, { - "name": "check_keccak_slot_3_pass(uint256)", - "exitcode": 0, - "num_models": 0, + "name": "check_createCalldata_Mock_4_excluding_view_fail()", + "exitcode": 1, + "num_models": 18, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_setup()", + "name": "check_createCalldata_Mock_4_excluding_view_pass()", "exitcode": 0, "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null - } - ], - "test/Store.t.sol:StoreTest": [ + }, { - "name": "check_load_nonexistent_account(uint256)", - "exitcode": 0, - "num_models": 0, + "name": "check_createCalldata_Mock_4_including_view_fail()", + "exitcode": 1, + "num_models": 20, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_store_Array(uint256,uint256)", + "name": "check_createCalldata_Mock_4_including_view_pass()", "exitcode": 0, "num_models": 0, "models": null, @@ -6701,16 +6734,16 @@ "num_bounded_loops": null }, { - "name": "check_store_Mapping(uint256,uint256)", - "exitcode": 0, - "num_models": 0, + "name": "check_createCalldata_Mock_interface_excluding_view_fail()", + "exitcode": 1, + "num_models": 18, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_store_Scalar(uint256)", + "name": "check_createCalldata_Mock_interface_excluding_view_pass()", "exitcode": 0, "num_models": 0, "models": null, @@ -6719,49 +6752,45 @@ "num_bounded_loops": null }, { - "name": "check_store_nonexistent_account()", - "exitcode": 3, - "num_models": 0, + "name": "check_createCalldata_Mock_interface_including_view_fail()", + "exitcode": 1, + "num_models": 20, "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, + "name": "check_createCalldata_Mock_interface_including_view_pass()", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, "num_bounded_loops": null }, { - "name": "check_StructArray((uint256,uint256)[],(uint256,uint256)[2])", + "name": "check_createCalldata_NestedArrays_1_fail()", "exitcode": 1, - "num_models": 1, + "num_models": 541, "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, + "name": "check_createCalldata_NestedArrays_1_pass()", + "exitcode": 0, + "num_models": 0, "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, + "name": "check_createCalldata_unknown_fail()", + "exitcode": 3, + "num_models": 0, "models": null, "num_paths": null, "time": null, diff --git a/tests/ffi/foundry.toml b/tests/ffi/foundry.toml index 57071666..7b3022dd 100644 --- a/tests/ffi/foundry.toml +++ b/tests/ffi/foundry.toml @@ -6,5 +6,5 @@ libs = ['../lib', 'lib'] evm_version = 'cancun' force = false -# compile options used by halmos (to prevent unnecessary recompilation when running forge test and halmos together) +# compile options used by dolmos (to prevent unnecessary recompilation when running forge test and dolmos together) extra_output = ["storageLayout", "metadata"] diff --git a/tests/regression/halmos.toml b/tests/regression/dolmos.toml similarity index 92% rename from tests/regression/halmos.toml rename to tests/regression/dolmos.toml index 4e4f3bd7..7761597e 100644 --- a/tests/regression/halmos.toml +++ b/tests/regression/dolmos.toml @@ -1,10 +1,4 @@ -# ___ ___ ___ ___ ___ ___ -# /\__\ /\ \ /\__\ /\__\ /\ \ /\ \ -# /:/__/_ /::\ \ /:/ / /::L_L_ /::\ \ /::\ \ -# /::\/\__\ /::\:\__\ /:/__/ /:/L:\__\ /:/\:\__\ /\:\:\__\ -# \/\::/ / \/\::/ / \:\ \ \/_/:/ / \:\/:/ / \:\:\/__/ -# /:/ / /:/ / \:\__\ /:/ / \::/ / \::/ / -# \/__/ \/__/ \/__/ \/__/ \/__/ \/__/ +# dolmos configuration, generated by `python -m dolmos.config` [global] diff --git a/tests/regression/foundry.toml b/tests/regression/foundry.toml index 07ac2d1a..6e09532c 100644 --- a/tests/regression/foundry.toml +++ b/tests/regression/foundry.toml @@ -9,5 +9,5 @@ optimizer = true force = false -# compile options used by halmos (to prevent unnecessary recompilation when running forge test and halmos together) +# compile options used by dolmos (to prevent unnecessary recompilation when running forge test and dolmos together) extra_output = ["storageLayout", "metadata"] diff --git a/tests/regression/src/Create.sol b/tests/regression/src/Create.sol index c5a6103d..fc32fc67 100644 --- a/tests/regression/src/Create.sol +++ b/tests/regression/src/Create.sol @@ -3,12 +3,12 @@ pragma solidity >=0.8.0 <0.9.0; contract Create { uint public value; - uint public immutable halmos; + uint public immutable dolmos; uint public initialized = 7; uint public constant const = 11; constructor(uint qed) { - halmos = qed; + dolmos = qed; } function set(uint x) public { diff --git a/tests/regression/test/Arith.t.sol b/tests/regression/test/Arith.t.sol index e49b53da..967240d4 100644 --- a/tests/regression/test/Arith.t.sol +++ b/tests/regression/test/Arith.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity >=0.8.0 <0.9.0; -/// @custom:halmos --solver bitwuzla-abs +/// @custom:dolmos --solver bitwuzla-abs contract ArithTest { function unchecked_div(uint x, uint y) public pure returns (uint ret) { assembly { diff --git a/tests/regression/test/AssertTest.t.sol b/tests/regression/test/AssertTest.t.sol index bfbfbc77..2b007c18 100644 --- a/tests/regression/test/AssertTest.t.sol +++ b/tests/regression/test/AssertTest.t.sol @@ -30,7 +30,7 @@ contract AssertTest is Test { function check_symbolic_revert(uint256 x) public { // reverts with Concat(0x4e487b71, p_x_uint256()) - // halmos only considers reverts with explicit revert codes so we expect a PASS here + // dolmos only considers reverts with explicit revert codes so we expect a PASS here // this is really to make sure we handle symbolic reverts gracefully if (x > 0) { bytes memory data = abi.encodeWithSignature("Panic(uint256)", x); diff --git a/tests/regression/test/Context.t.sol b/tests/regression/test/Context.t.sol index 513289fa..f76870cb 100644 --- a/tests/regression/test/Context.t.sol +++ b/tests/regression/test/Context.t.sol @@ -40,7 +40,7 @@ contract Context is Test { } } else if (mode == 9) { vm.prank(address(0)); - vm.prank(address(0)); // HalmosException + vm.prank(address(0)); // DolmosException } else if (mode == 10) { fail(); } @@ -179,7 +179,7 @@ contract ContextTest is Test { ensure_test_context(); } - function check_create_halmos_exception() public payable { + function check_create_dolmos_exception() public payable { assert(returndatasize() == 0); // empty initial returndata ensure_test_context(); @@ -198,7 +198,7 @@ contract ContextTest is Test { _check_call0(mode0); } - function check_call0_halmos_exception() public payable { + function check_call0_dolmos_exception() public payable { _check_call0(9); } @@ -231,7 +231,7 @@ contract ContextTest is Test { _check_call1(mode1, mode0); } - function check_call1_halmos_exception(uint mode1) public payable { + function check_call1_dolmos_exception(uint mode1) public payable { _check_call1(mode1, 9); } diff --git a/tests/regression/test/Counter.t.sol b/tests/regression/test/Counter.t.sol index 0f0ab95b..e8a7eb5c 100644 --- a/tests/regression/test/Counter.t.sol +++ b/tests/regression/test/Counter.t.sol @@ -4,7 +4,7 @@ pragma solidity >=0.8.0 <0.9.0; import "../src/Counter.sol"; import {SymTest} from "halmos-cheatcodes/SymTest.sol"; -/// @custom:halmos --loop 4 +/// @custom:dolmos --loop 4 contract CounterTest is Counter, SymTest { function setUp() public { svm.enableSymbolicStorage(address(this)); diff --git a/tests/regression/test/Create.t.sol b/tests/regression/test/Create.t.sol index 4c32e305..687f4a9d 100644 --- a/tests/regression/test/Create.t.sol +++ b/tests/regression/test/Create.t.sol @@ -23,7 +23,7 @@ contract CreateTest is Test { } function check_immutable() public { - assertEq(create.halmos(), 0x220E); + assertEq(create.dolmos(), 0x220E); } function check_initialized() public { diff --git a/tests/regression/test/Create3.t.sol b/tests/regression/test/Create3.t.sol index 20983ad7..6d7cddd6 100644 --- a/tests/regression/test/Create3.t.sol +++ b/tests/regression/test/Create3.t.sol @@ -4,7 +4,7 @@ pragma solidity >=0.8.0 <0.9.0; import "forge-std/Test.sol"; /// @notice minimal CREATE3 library, as found in 0xsequence/create3, solmate and solady -/// @dev see https://github.com/a16z/halmos/issues/217 +/// @dev see upstream halmos issue #217 library Create3 { bytes internal constant PROXY_CHILD_BYTECODE = hex"67363d3d37363d34f03d5260086018f3"; diff --git a/tests/regression/test/Foundry.t.sol b/tests/regression/test/Foundry.t.sol index 80f9b0e2..d2ac0a3e 100644 --- a/tests/regression/test/Foundry.t.sol +++ b/tests/regression/test/Foundry.t.sol @@ -185,7 +185,7 @@ contract TestNotEtchFriendly is Test { NotEtchFriendly target; function setUp() public { - /// @dev this is supported in foundry, but not halmos (can't vm.store to uninitialized account) + /// @dev this is supported in foundry, but not dolmos (can't vm.store to uninitialized account) // make address(this) the owner of the yet-to-be-deployed contract // vm.store(address(42), 0, bytes32(uint256(uint160(address(this))))); diff --git a/tests/regression/test/Getter.t.sol b/tests/regression/test/Getter.t.sol index 9eb9bdcf..fec3c051 100644 --- a/tests/regression/test/Getter.t.sol +++ b/tests/regression/test/Getter.t.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity >=0.8.0 <0.9.0; -// from https://github.com/a16z/halmos/issues/82 +// from upstream halmos issue #82 -/// @custom:halmos --storage-layout=generic +/// @custom:dolmos --storage-layout=generic contract GetterTest { uint256[3] public v; uint w; diff --git a/tests/regression/test/Invalid.t.sol b/tests/regression/test/Invalid.t.sol index 2014ec6f..7792ad5a 100644 --- a/tests/regression/test/Invalid.t.sol +++ b/tests/regression/test/Invalid.t.sol @@ -5,12 +5,12 @@ contract OldCompilerTest { 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. + assert(false); // old compiler versions don't revert with panic; instead, they run invalid opcode, which dolmos ignores, resulting in no error here. } 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. + myAssert(false); // you can use your own assertion that panic-reverts if assertion fails, when using dolmos for old version code. } function myAssert(bool cond) internal pure { diff --git a/tests/regression/test/InvariantProbes.t.sol b/tests/regression/test/InvariantProbes.t.sol index ae99ecf1..4af08e0a 100644 --- a/tests/regression/test/InvariantProbes.t.sol +++ b/tests/regression/test/InvariantProbes.t.sol @@ -43,7 +43,7 @@ contract InvariantProbesTest is Test { } // XFAIL: we should report failures when asserts are hit in the target contract - /// @custom:halmos --invariant-depth 4 + /// @custom:dolmos --invariant-depth 4 function invariant_probes_found() public { assertGe(c.num(), 0); } diff --git a/tests/regression/test/InvariantTarget.t.sol b/tests/regression/test/InvariantTarget.t.sol index bc369aad..408f75c1 100644 --- a/tests/regression/test/InvariantTarget.t.sol +++ b/tests/regression/test/InvariantTarget.t.sol @@ -86,7 +86,7 @@ contract InvariantTargetTest_target_contract_this is InvariantTargetTest { c = new C(); d = new D(); - // https://github.com/a16z/halmos/issues/506 + // upstream halmos issue #506 // C and D are not set as a targets, because targetContract(address(this)) is used targetContract(address(this)); } diff --git a/tests/regression/test/InvariantTargetThis.t.sol b/tests/regression/test/InvariantTargetThis.t.sol index 90330345..0cff5170 100644 --- a/tests/regression/test/InvariantTargetThis.t.sol +++ b/tests/regression/test/InvariantTargetThis.t.sol @@ -38,13 +38,13 @@ contract InvariantTargetBase { } } -// https://github.com/a16z/halmos/issues/514 +// upstream halmos issue #514 // special functions like test_, check_, setUp(), invariant_, etc. // should not be considered as targets when selecting targetContract(address(this)) // FIXME: this test generates 1 model with the solidity storage layout, but 2 models (at depth 2) // with the generic storage layout, so we set depth to 1 for consistent results -/// @custom:halmos --invariant-depth 1 +/// @custom:dolmos --invariant-depth 1 contract InvariantTargetThis is InvariantTargetBase, Test { // not a test contract, but deployed by the test contract // so it should be included, and its target functions included even if they diff --git a/tests/regression/test/MegaMem.t.sol b/tests/regression/test/MegaMem.t.sol index 2657d5ee..917ab9ed 100644 --- a/tests/regression/test/MegaMem.t.sol +++ b/tests/regression/test/MegaMem.t.sol @@ -6,7 +6,7 @@ import {console2} from "forge-std/console2.sol"; import {SymTest} from "halmos-cheatcodes/SymTest.sol"; -/// @dev This checks that halmos can handle large memory operations without raising +/// @dev This checks that dolmos can handle large memory operations without raising /// internal errors like "cannot fit 'int' into an index-sized integer". /// Instead, we should catch the error and revert the current path, but letting other /// paths continue. This is why these tests do a symbolic coinflip, we case split: diff --git a/tests/regression/test/Natspec.t.sol b/tests/regression/test/Natspec.t.sol index 1e4000bd..d94551dd 100644 --- a/tests/regression/test/Natspec.t.sol +++ b/tests/regression/test/Natspec.t.sol @@ -16,7 +16,7 @@ contract NatspecTestNone { } } -/// @custom:halmos --loop 3 +/// @custom:dolmos --loop 3 contract NatspecTestContract { Loop l; @@ -35,7 +35,7 @@ contract NatspecTestContract { contract NatspecTestSetup { Loop l; - /// @custom:halmos --loop 3 + /// @custom:dolmos --loop 3 function setUp() public { l = new Loop(); } @@ -55,11 +55,11 @@ contract NatspecTestFunction { l = new Loop(); } - /// @custom:halmos --loop 3 + /// @custom:dolmos --loop 3 function check_Loop3(uint n) public view { assert(l.iter(n) <= 3); // pass } - /// @custom:halmos --loop 3 + /// @custom:dolmos --loop 3 function check_Loop3Fail(uint n) public view { assert(l.iter(n) <= 2); // fail } @@ -72,7 +72,7 @@ contract NatspecTestFunction { } } -/// @custom:halmos --loop 4 +/// @custom:dolmos --loop 4 contract NatspecTestOverwrite { Loop l; @@ -87,16 +87,38 @@ contract NatspecTestOverwrite { assert(l.iter(n) <= 3); // fail // inherited from contract } - /// @custom:halmos --loop 3 + /// @custom:dolmos --loop 3 function check_Loop3(uint n) public view { assert(l.iter(n) <= 3); // pass // overwrite } - /// @custom:halmos --loop 3 + /// @custom:dolmos --loop 3 function check_Loop3Fail(uint n) public view { assert(l.iter(n) <= 2); // fail // overwrite } } +// the legacy tag used by test suites written for halmos is still honored +/// @custom:halmos --loop 3 +contract NatspecTestLegacyTag { + Loop l; + + function setUp() public { + l = new Loop(); + } + + function check_Loop3(uint n) public view { + assert(l.iter(n) <= 3); // pass // inherited from contract (legacy tag) + } + function check_Loop3Fail(uint n) public view { + assert(l.iter(n) <= 2); // fail // inherited from contract (legacy tag) + } + + /// @custom:halmos --loop 4 + function check_Loop4(uint n) public view { + assert(l.iter(n) <= 4); // pass // legacy tag on the function + } +} + contract Loop { function iter(uint n) public pure returns (uint) { uint cnt = 0; diff --git a/tests/regression/test/Panic.t.sol b/tests/regression/test/Panic.t.sol index 0d67aa45..0e6fc282 100644 --- a/tests/regression/test/Panic.t.sol +++ b/tests/regression/test/Panic.t.sol @@ -43,7 +43,7 @@ contract PanicTest is Test { } } - /// @custom:halmos --panic-error-codes 0x00 + /// @custom:dolmos --panic-error-codes 0x00 function check_panic_0_fail(bool x) public { if (x) _panic(0); } @@ -56,7 +56,7 @@ contract PanicTest is Test { if (x) _panic(1); // default error code is 1 } - /// @custom:halmos --panic-error-codes 0x00 + /// @custom:dolmos --panic-error-codes 0x00 function check_panic_1_pass(bool x) public { if (x) _panic(1); } @@ -65,17 +65,17 @@ contract PanicTest is Test { // multiple error codes // - /// @custom:halmos --panic-error-codes 0x02,0x03 + /// @custom:dolmos --panic-error-codes 0x02,0x03 function check_panic_2_or_3_fail_2(bool x) public { if (x) _panic(2); } - /// @custom:halmos --panic-error-codes 0x02,0x03 + /// @custom:dolmos --panic-error-codes 0x02,0x03 function check_panic_2_or_3_fail_3(bool x) public { if (x) _panic(3); } - /// @custom:halmos --panic-error-codes * + /// @custom:dolmos --panic-error-codes * function check_panic_any_fail_4(bool x) public { if (x) _panic(4); } @@ -84,7 +84,7 @@ contract PanicTest is Test { // old compiler revert // - /// @custom:halmos --panic-error-codes 0x05 + /// @custom:dolmos --panic-error-codes 0x05 function check_panic_5_fail_old(bool x) public { if (x) _panic_old_compiler(5); } @@ -94,25 +94,25 @@ contract PanicTest is Test { // // binary - /// @custom:halmos --panic-error-codes 0b111 + /// @custom:dolmos --panic-error-codes 0b111 function check_panic_7_fail(bool x) public { if (x) _panic(7); } // octal - /// @custom:halmos --panic-error-codes 0o10 + /// @custom:dolmos --panic-error-codes 0o10 function check_panic_8_fail(bool x) public { if (x) _panic(8); } // decimal - /// @custom:halmos --panic-error-codes 10 + /// @custom:dolmos --panic-error-codes 10 function check_panic_10_fail(bool x) public { if (x) _panic(10); } // hex - /// @custom:halmos --panic-error-codes 0x10 + /// @custom:dolmos --panic-error-codes 0x10 function check_panic_16_fail(bool x) public { if (x) _panic(16); } @@ -121,7 +121,7 @@ contract PanicTest is Test { // panic error code semantics // - /// @custom:halmos --panic-error-codes 0x11 + /// @custom:dolmos --panic-error-codes 0x11 function check_panic_0x11_fail(uint x) public returns (uint) { // 0x11: overflow return x + 1; // counterexample: x == 2^256 - 1 @@ -131,7 +131,7 @@ contract PanicTest is Test { return x + 1; } - /// @custom:halmos --panic-error-codes 0x12 + /// @custom:dolmos --panic-error-codes 0x12 function check_panic_0x12_fail(uint x) public returns (uint) { // 0x12: div-by-zero return 10 / x; // counterexample: x == 0 @@ -145,7 +145,7 @@ contract PanicTest is Test { // panic propagation // - /// @custom:halmos --panic-error-codes 0x11 + /// @custom:dolmos --panic-error-codes 0x11 function check_panic_inc_fail(uint x) public returns (uint) { // 0x11: overflow return c.inc(x); // counterexample: x == 2^256 - 1 @@ -155,7 +155,7 @@ contract PanicTest is Test { // capturing different types of reverts // - /// @custom:halmos --panic-error-codes 0x11 + /// @custom:dolmos --panic-error-codes 0x11 function check_panic_inc_overflow_fail(uint x) public returns (uint) { // fail due to overflow, Panic(0x11) // but no assertion violation @@ -175,7 +175,7 @@ contract PanicTest is Test { } } - /// @custom:halmos --panic-error-codes 0x11 + /// @custom:dolmos --panic-error-codes 0x11 function check_panic_inc_assert_pass(uint x) public returns (uint) { // pass because the assertion violation error code is ignored, and there's no overflow unchecked { @@ -183,7 +183,7 @@ contract PanicTest is Test { } } - /// @custom:halmos --panic-error-codes 0xff + /// @custom:dolmos --panic-error-codes 0xff function check_panic_inc_cheatcode_fail(uint x) public returns (uint) { // fail even if Panic(1) is ignored, // because assertion cheatcode failures are handled separately and are always captured diff --git a/tests/regression/test/Revert.t.sol b/tests/regression/test/Revert.t.sol index cc9b5d10..babf2789 100644 --- a/tests/regression/test/Revert.t.sol +++ b/tests/regression/test/Revert.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity >=0.8.0 <0.9.0; -// from https://github.com/a16z/halmos/issues/109 +// from upstream halmos issue #109 import "forge-std/Test.sol"; @@ -86,7 +86,7 @@ contract CTest is Test { (bool success, ) = address(c).call{value: amount}(abi.encodeWithSignature("deposit(bool)", false)); // we are looking for a counterexample here - // i.e., halmos should find the case amount > balance + // i.e., dolmos should find the case amount > balance assert(success); } @@ -97,7 +97,7 @@ contract CTest is Test { (bool success, ) = address(42).call{value: amount}(""); // we are looking for a counterexample here - // i.e., halmos should find the case amount > balance + // i.e., dolmos should find the case amount > balance assert(success); } diff --git a/tests/regression/test/Send.t.sol b/tests/regression/test/Send.t.sol index 5b6eef72..7c5f64e8 100644 --- a/tests/regression/test/Send.t.sol +++ b/tests/regression/test/Send.t.sol @@ -8,7 +8,7 @@ contract C { constructor() payable { } } -/// @custom:halmos --solver-timeout-assertion 0 +/// @custom:dolmos --solver-timeout-assertion 0 contract SendTest is Test, SymTest { address sender; address payable receiver; @@ -96,7 +96,7 @@ contract SendTest is Test, SymTest { } function check_create(uint amount, bytes32 salt, uint mode) public { - // note: deployer is set to concrete, to prevent halmos from treating the contract creation target as aliasing with the deployer + // note: deployer is set to concrete, to prevent dolmos from treating the contract creation target as aliasing with the deployer address deployer = address(0xbeef); vm.deal(deployer, svm.createUint(96, "deployer.balance")); diff --git a/tests/regression/test/SetupPlus.t.sol b/tests/regression/test/SetupPlus.t.sol index e527029f..f78a535f 100644 --- a/tests/regression/test/SetupPlus.t.sol +++ b/tests/regression/test/SetupPlus.t.sol @@ -18,7 +18,7 @@ contract SetupPlusTest { a = new A(11, 200); } - // if setUpSymbolic() is provided, Halmos uses setUpSymbolic() instead of setUp(). + // if setUpSymbolic() is provided, Dolmos uses setUpSymbolic() instead of setUp(). // setUpSymbolic() is symbolically executed. // if multiple setUpSymbolic() functions are provided, the last one in the lexicographical order will be used. diff --git a/tests/regression/test/Sha3.t.sol b/tests/regression/test/Sha3.t.sol index 054c5839..649ec698 100644 --- a/tests/regression/test/Sha3.t.sol +++ b/tests/regression/test/Sha3.t.sol @@ -90,7 +90,7 @@ contract Sha3Test is Test, SymTest { } // we assume that the lower 160-bit parts do not collide - // see: https://github.com/a16z/halmos/issues/347 + // see: upstream halmos issue #347 function check_address_collision_pass(uint256 x, uint256 y) public { vm.assume(x != y); assertNotEq(to_address(x), to_address(y)); // pass diff --git a/tests/regression/test/Signature.t.sol b/tests/regression/test/Signature.t.sol index 67f8fd08..da114431 100644 --- a/tests/regression/test/Signature.t.sol +++ b/tests/regression/test/Signature.t.sol @@ -398,7 +398,7 @@ contract SignatureTest is SymTest, Test { } // TODO: remove the following option after fixing issue in case of empty keyName - /// @custom:halmos --array-lengths keyName={65,1024} + /// @custom:dolmos --array-lengths keyName={65,1024} function check_makeAddrAndKey_vmsign_ecrecover_e2e_symbolic( string memory keyName, bytes32 digest diff --git a/tests/regression/test/SmolWETH.t.sol b/tests/regression/test/SmolWETH.t.sol index c8928575..4b60b750 100644 --- a/tests/regression/test/SmolWETH.t.sol +++ b/tests/regression/test/SmolWETH.t.sol @@ -39,7 +39,7 @@ contract SmolWETH { } } -/// @custom:halmos --storage-layout=generic +/// @custom:dolmos --storage-layout=generic contract SmolWETHTest is Test, SymTest { SmolWETH weth; diff --git a/tests/regression/test/Snapshot.t.sol b/tests/regression/test/Snapshot.t.sol index a203f244..3beb9e6c 100644 --- a/tests/regression/test/Snapshot.t.sol +++ b/tests/regression/test/Snapshot.t.sol @@ -14,7 +14,7 @@ contract C { } } -/// @custom:halmos --storage-layout solidity +/// @custom:dolmos --storage-layout solidity contract SnapshotTest is SymTest, Test { C c; @@ -22,7 +22,7 @@ contract SnapshotTest is SymTest, Test { c = new C(); } - // NOTE: In halmos, the state snapshot ID is constructed by concatenating three hashes of: balance (64 bits), code (64 bits), and storage (128 bits). + // NOTE: In dolmos, the state snapshot ID is constructed by concatenating three hashes of: balance (64 bits), code (64 bits), and storage (128 bits). function check_snapshot() public { uint storage0 = svm.snapshotStorage(address(c)); diff --git a/tests/regression/test/Solver.t.sol b/tests/regression/test/Solver.t.sol index 496665b3..dcf7edaa 100644 --- a/tests/regression/test/Solver.t.sol +++ b/tests/regression/test/Solver.t.sol @@ -11,9 +11,9 @@ contract SolverTest is SymTest, Test { numbers = new uint[](5); // shouldn't generate loop bounds warning } - /// @custom:halmos --solver-timeout-assertion 1 + /// @custom:dolmos --solver-timeout-assertion 1 function check_too_many_open_files() public { - // regression test for too many open files error: https://github.com/a16z/halmos/issues/523 + // regression test for too many open files error: upstream halmos issue #523 // this test simulates a situation where many solver processes are killed due to timeout. // if file descriptors are not properly closed, this test will fail with "Too many open files" error. if (svm.createBool("*")) { some_hard_query(); } else { some_hard_query(); } diff --git a/tests/regression/test/StdAssertTest.t.sol b/tests/regression/test/StdAssertTest.t.sol index db0b9746..6d148e47 100644 --- a/tests/regression/test/StdAssertTest.t.sol +++ b/tests/regression/test/StdAssertTest.t.sol @@ -43,14 +43,14 @@ contract StdAssertPassTest is Test { } // times out after 1min with yices and --cache-solver - /// @custom:halmos --solver z3 + /// @custom:dolmos --solver z3 function check_assertEq(string memory x, string memory y) public { vm.assume(keccak256(bytes(x)) == keccak256(bytes(y))); assertEq(x, y); } // times out after 1min with yices and --cache-solver - /// @custom:halmos --solver z3 + /// @custom:dolmos --solver z3 function check_assertEq(bytes memory x, bytes memory y) public { vm.assume(keccak256(x) == keccak256(y)); assertEq(x, y); @@ -517,14 +517,14 @@ contract StdAssertFailLogTest is Test { } // times out after 1min with yices and --cache-solver - /// @custom:halmos --solver z3 + /// @custom:dolmos --solver z3 function check_assertNotEq(string memory x, string memory y) public { vm.assume(!(keccak256(bytes(x)) != keccak256(bytes(y)))); assertNotEq(x, y, ""); } // times out after 1min with yices and --cache-solver - /// @custom:halmos --solver z3 + /// @custom:dolmos --solver z3 function check_assertNotEq(bytes memory x, bytes memory y) public { vm.assume(!(keccak256(x) != keccak256(y))); assertNotEq(x, y, ""); diff --git a/tests/regression/test/Storage5.t.sol b/tests/regression/test/Storage5.t.sol index f1518a75..1cd826b1 100644 --- a/tests/regression/test/Storage5.t.sol +++ b/tests/regression/test/Storage5.t.sol @@ -44,7 +44,7 @@ contract Storage5Test { // the form of an addition, `keccak(x) + (index % y)`, is automatically // simplified to a concatenation form when the keccak value ends with // zero bits and y is a power of two. This simplified form is not - // currently supported by the decoding logic of halmos. + // currently supported by the decoding logic of dolmos. assert(arr0[index % 1] == 0); // sload(keccak(0) + (index % 1)) assert(arr0[index % 2] == 0); // sload(keccak(0) + (index % 2)) diff --git a/tests/regression/test/StorageSlot.t.sol b/tests/regression/test/StorageSlot.t.sol index a111debc..07b0b7ea 100644 --- a/tests/regression/test/StorageSlot.t.sol +++ b/tests/regression/test/StorageSlot.t.sol @@ -29,8 +29,8 @@ contract StorageSlotTest { assert(map[one] == value); // sload with keccak expression } - // this test passes because halmos internally precomputes the slots for m[0] and m[1], for any m where slot(m) < 256. - // in general, however, directly initializing storage with a precomputed hash is not supported by halmos. see the test below. + // this test passes because dolmos internally precomputes the slots for m[0] and m[1], for any m where slot(m) < 256. + // in general, however, directly initializing storage with a precomputed hash is not supported by dolmos. see the test below. function check_keccak_slot_2_pass(uint value) public { sstore(slot_map_one, value); // sstore with precomputed hash @@ -38,7 +38,7 @@ contract StorageSlotTest { assert(map[one] == value); // sload with keccak expression } - // this test failed due to m[2] beyond the scope of halmos internal precomputation. see the above test for comparison. + // this test failed due to m[2] beyond the scope of dolmos internal precomputation. see the above test for comparison. function check_keccak_slot_2_fail(uint value) public { sstore(slot_map_two, value); // sstore with precomputed hash diff --git a/tests/regression/test/Store.t.sol b/tests/regression/test/Store.t.sol index 67c5602b..300e266a 100644 --- a/tests/regression/test/Store.t.sol +++ b/tests/regression/test/Store.t.sol @@ -50,7 +50,7 @@ contract StoreTest is Test { function check_store_nonexistent_account() public { // vm.store() is not allowed for a nonexistent account - vm.store(address(0xdeadbeef), bytes32(0), bytes32(0)); // HalmosException + vm.store(address(0xdeadbeef), bytes32(0), bytes32(0)); // DolmosException } function check_load_nonexistent_account(uint slot) public { diff --git a/tests/regression/test/Struct.t.sol b/tests/regression/test/Struct.t.sol index fbb0ed71..7b465e3e 100644 --- a/tests/regression/test/Struct.t.sol +++ b/tests/regression/test/Struct.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity >=0.8.0 <0.9.0; -/// @custom:halmos --solver-timeout-assertion 0 +/// @custom:dolmos --solver-timeout-assertion 0 contract StructTest { struct Point { uint x; @@ -15,7 +15,7 @@ contract StructTest { assert(result == 0); // expected to fail and generate a counterexample that incorporates all calldata symbols } - /// @custom:halmos --array-lengths p=1 + /// @custom:dolmos --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 { @@ -30,7 +30,7 @@ contract StructTest { assert(result == 0); // expected to fail and generate a counterexample that incorporates all calldata symbols } - /// @custom:halmos --array-lengths p=1,p[0]=1 --default-array-lengths 2 + /// @custom:dolmos --array-lengths p=1,p[0]=1 --default-array-lengths 2 function check_StructArrayArray( Point[][] memory p, Point[2][] memory q, @@ -69,7 +69,7 @@ contract StructTest { } } -/// @custom:halmos --solver-timeout-assertion 0 --default-array-lengths 2 +/// @custom:dolmos --solver-timeout-assertion 0 --default-array-lengths 2 contract StructTest2 { struct P { uint x; @@ -86,7 +86,7 @@ contract StructTest2 { P[][] f6; } - /// @custom:halmos --array-lengths s=1 + /// @custom:dolmos --array-lengths s=1 function check_S(P memory p, S[] memory s) public pure returns (uint result) { unchecked { result += sum_P(p); diff --git a/tests/regression/test/HalmosCheatCode.t.sol b/tests/regression/test/SvmCheatCode.t.sol similarity index 96% rename from tests/regression/test/HalmosCheatCode.t.sol rename to tests/regression/test/SvmCheatCode.t.sol index e1a3b760..1efb8339 100644 --- a/tests/regression/test/HalmosCheatCode.t.sol +++ b/tests/regression/test/SvmCheatCode.t.sol @@ -10,7 +10,7 @@ contract Beep { } } -contract HalmosCheatCodeTest is SymTest, Test { +contract SvmCheatCodeTest is SymTest, Test { function check_createUint() public { uint x = svm.createUint(256, 'x'); uint y = svm.createUint(160, 'y'); @@ -198,12 +198,12 @@ contract HalmosCheatCodeTest is SymTest, Test { } function check_random_uint_range_symbolic_fail(uint256 min, uint256 max) public { - // expected to fail with HalmosException + // expected to fail with DolmosException vm.randomUint(min, max); } function check_random_uint_range_max_greaterthan_min_fail() public { - // expected to fail with HalmosException + // expected to fail with DolmosException vm.randomUint(1, 0); } @@ -262,15 +262,15 @@ contract HalmosCheatCodeTest is SymTest, Test { function check_enableSymbolicStorage_nonexistent() public { // symbolic storage is not allowed for a nonexistent account - svm.enableSymbolicStorage(address(0xdeadbeef)); // HalmosException + svm.enableSymbolicStorage(address(0xdeadbeef)); // DolmosException } function check_setArbitraryStorage_nonexistent() public { // Arbitrary Storage is not allowed for a nonexistent account - vm.setArbitraryStorage(address(0xdeadbeef)); // HalmosException + vm.setArbitraryStorage(address(0xdeadbeef)); // DolmosException } - /// @custom:halmos --array-lengths name=1 + /// @custom:dolmos --array-lengths name=1 function check_extract_string_argument_fail(string memory name) public { uint x = svm.createUint256(name); console.log(x); @@ -606,8 +606,8 @@ contract HalmosCheatCodeTest is SymTest, Test { } } -/// @custom:halmos --default-bytes-lengths 0,65 -contract HalmosCreateCalldataTest is SymTest, Test { +/// @custom:dolmos --default-bytes-lengths 0,65 +contract SvmCreateCalldataTest is SymTest, Test { Beep beep = new Beep(); Mock mock = new Mock(false); // use Mock(true) for debugging @@ -627,12 +627,12 @@ contract HalmosCreateCalldataTest is SymTest, Test { } function check_createCalldata_Beep_1_excluding_pure() public { - bytes memory data = svm.createCalldata("HalmosCheatCode.t.sol", "Beep"); + bytes memory data = svm.createCalldata("SvmCheatCode.t.sol", "Beep"); _check_createCalldata_Beep(data); // fail because the only function in Beep is pure, which is excluded in createCalldata() } function check_createCalldata_Beep_1_including_pure() public { - bytes memory data = svm.createCalldata("HalmosCheatCode.t.sol", "Beep", true); + bytes memory data = svm.createCalldata("SvmCheatCode.t.sol", "Beep", true); _check_createCalldata_Beep(data); } @@ -699,29 +699,29 @@ contract HalmosCreateCalldataTest is SymTest, Test { } function check_createCalldata_Mock_3_pass() public { - bytes memory data = svm.createCalldata("HalmosCheatCode.t.sol", "Mock"); + bytes memory data = svm.createCalldata("SvmCheatCode.t.sol", "Mock"); _check_createCalldata_Mock(data, true); } function check_createCalldata_Mock_3_fail() public { - bytes memory data = svm.createCalldata("HalmosCheatCode.t.sol", "Mock"); + bytes memory data = svm.createCalldata("SvmCheatCode.t.sol", "Mock"); _check_createCalldata_Mock(data, false); } function check_createCalldata_Mock_4_excluding_view_pass() public { - bytes memory data = svm.createCalldata("HalmosCheatCode.t.sol", "Mock", false); + bytes memory data = svm.createCalldata("SvmCheatCode.t.sol", "Mock", false); _check_createCalldata_Mock(data, true); } function check_createCalldata_Mock_4_excluding_view_fail() public { - bytes memory data = svm.createCalldata("HalmosCheatCode.t.sol", "Mock", false); + bytes memory data = svm.createCalldata("SvmCheatCode.t.sol", "Mock", false); _check_createCalldata_Mock(data, false); } function check_createCalldata_Mock_4_including_view_pass() public { - bytes memory data = svm.createCalldata("HalmosCheatCode.t.sol", "Mock", true); + bytes memory data = svm.createCalldata("SvmCheatCode.t.sol", "Mock", true); _check_createCalldata_Mock(data, true); } function check_createCalldata_Mock_4_including_view_fail() public { - bytes memory data = svm.createCalldata("HalmosCheatCode.t.sol", "Mock", true); + bytes memory data = svm.createCalldata("SvmCheatCode.t.sol", "Mock", true); _check_createCalldata_Mock(data, false); } diff --git a/tests/regression/test/TStore.t.sol b/tests/regression/test/TStore.t.sol index 619b116d..99fdf527 100644 --- a/tests/regression/test/TStore.t.sol +++ b/tests/regression/test/TStore.t.sol @@ -31,7 +31,7 @@ contract C { } } -/// @custom:halmos --storage-layout generic +/// @custom:dolmos --storage-layout generic contract TStoreTest is Test { C c; diff --git a/tests/regression/test/Token.t.sol b/tests/regression/test/Token.t.sol index 8218e15f..5c4111d7 100644 --- a/tests/regression/test/Token.t.sol +++ b/tests/regression/test/Token.t.sol @@ -4,7 +4,7 @@ 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 +/// @custom:dolmos --solver-timeout-assertion 0 contract TokenTest is SymTest, Test { Token token; diff --git a/tests/regression/test/UniswapPairAddress.t.sol b/tests/regression/test/UniswapPairAddress.t.sol index ee5a776a..d6406eed 100644 --- a/tests/regression/test/UniswapPairAddress.t.sol +++ b/tests/regression/test/UniswapPairAddress.t.sol @@ -77,7 +77,7 @@ contract UniswapPairAddressTest is Test { console.log("uniswapV2Factory", address(uniswapV2Factory)); } - // https://github.com/a16z/halmos/issues/393 + // upstream halmos issue #393 function check_inferred_address_matches_deployed_address() public { // uses the actual hash of the init code address deployedPair = uniswapV2Factory.createPair(address(tokenA), address(tokenB)); diff --git a/tests/solver/foundry.toml b/tests/solver/foundry.toml index 57071666..7b3022dd 100644 --- a/tests/solver/foundry.toml +++ b/tests/solver/foundry.toml @@ -6,5 +6,5 @@ libs = ['../lib', 'lib'] evm_version = 'cancun' force = false -# compile options used by halmos (to prevent unnecessary recompilation when running forge test and halmos together) +# compile options used by dolmos (to prevent unnecessary recompilation when running forge test and dolmos together) extra_output = ["storageLayout", "metadata"] diff --git a/tests/solver/test/Math.t.sol b/tests/solver/test/Math.t.sol index a56f3255..c1f470ed 100644 --- a/tests/solver/test/Math.t.sol +++ b/tests/solver/test/Math.t.sol @@ -10,7 +10,7 @@ contract MathTest { } } - /// @custom:halmos --solver-timeout-assertion 10000 + /// @custom:dolmos --solver-timeout-assertion 10000 function check_deposit(uint a, uint A1, uint S1) public pure { uint s = (a * S1) / A1; @@ -21,7 +21,7 @@ contract MathTest { assert(A1 * S2 <= A2 * S1); // no counterexample } - /// @custom:halmos --solver-timeout-assertion 0 + /// @custom:dolmos --solver-timeout-assertion 0 function check_mint(uint s, uint A1, uint S1) public pure { uint a = (s * A1) / S1; diff --git a/tests/solver/test/Solver.t.sol b/tests/solver/test/Solver.t.sol index df3fdce6..80a2e32e 100644 --- a/tests/solver/test/Solver.t.sol +++ b/tests/solver/test/Solver.t.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: AGPL-3.0 pragma solidity >=0.8.0 <0.9.0; -// from https://github.com/a16z/halmos/issues/57 +// from upstream halmos issue #57 -/// @custom:halmos --solver-timeout-assertion 10000 +/// @custom:dolmos --solver-timeout-assertion 10000 contract SolverTest { function foo(uint x) public pure returns (uint) { diff --git a/tests/test_bitvec.py b/tests/test_bitvec.py index 1d201f21..8bd21b43 100644 --- a/tests/test_bitvec.py +++ b/tests/test_bitvec.py @@ -13,11 +13,11 @@ simplify, ) -from halmos.bitvec import FALSE, TRUE -from halmos.bitvec import HalmosBitVec as BV -from halmos.bitvec import HalmosBool as Bool -from halmos.bytevec import Chunk -from halmos.exceptions import NotConcreteError +from dolmos.bitvec import FALSE, TRUE +from dolmos.bitvec import DolmosBitVec as BV +from dolmos.bitvec import DolmosBool as Bool +from dolmos.bytevec import Chunk +from dolmos.exceptions import NotConcreteError a, b = Bool("a"), Bool("b") x, y = BV("x"), BV("y") diff --git a/tests/test_bytevec.py b/tests/test_bytevec.py index 865d6159..c7708a0e 100644 --- a/tests/test_bytevec.py +++ b/tests/test_bytevec.py @@ -1,8 +1,8 @@ import pytest from z3 import BitVec, BitVecVal, Concat, Extract, eq -from halmos.bytevec import ByteVec, Chunk, defrag -from halmos.utils import concat, extract_bytes +from dolmos.bytevec import ByteVec, Chunk, defrag +from dolmos.utils import concat, extract_bytes @pytest.fixture diff --git a/tests/test_cli.py b/tests/test_cli.py index edde7ecd..f9f61cd1 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -8,10 +8,10 @@ eq, ) -from halmos.bitvec import HalmosBitVec as BV -from halmos.calldata import str_abi -from halmos.sevm import Contract, Instruction -from halmos.utils import EVM, hexify +from dolmos.bitvec import DolmosBitVec as BV +from dolmos.calldata import str_abi +from dolmos.sevm import Contract, Instruction +from dolmos.utils import EVM, hexify @pytest.fixture diff --git a/tests/test_config.py b/tests/test_config.py index ac4a8f63..1c381593 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -4,7 +4,7 @@ import pytest -from halmos.config import ( +from dolmos.config import ( Config, ConfigSource, ParseArrayLengths, @@ -14,7 +14,7 @@ default_config, resolve_config_files, ) -from halmos.config import ( +from dolmos.config import ( toml_parser as get_toml_parser, ) diff --git a/tests/test_halmos.py b/tests/test_dolmos.py similarity index 86% rename from tests/test_halmos.py rename to tests/test_dolmos.py index d43ecff3..a7e70f47 100644 --- a/tests/test_halmos.py +++ b/tests/test_dolmos.py @@ -4,10 +4,10 @@ import pytest -from halmos.__main__ import _main -from halmos.bytevec import ByteVec -from halmos.sevm import con -from halmos.traces import rendered_calldata +from dolmos.__main__ import _main +from dolmos.bytevec import ByteVec +from dolmos.sevm import con +from dolmos.traces import rendered_calldata @pytest.mark.parametrize( @@ -52,8 +52,8 @@ "long:examples/tokens/ERC721", ), ) -def test_main(cmd, expected_path, halmos_options): - actual = dataclasses.asdict(_main(cmd + halmos_options.split())) +def test_main(cmd, expected_path, dolmos_options): + actual = dataclasses.asdict(_main(cmd + dolmos_options.split())) with open(expected_path, encoding="utf8") as f: expected = json.load(f) assert expected["exitcode"] == actual["exitcode"] @@ -63,10 +63,10 @@ def test_main(cmd, expected_path, halmos_options): @pytest.mark.skipif( shutil.which("vyper") is None, reason="vyper compiler not found in PATH" ) -def test_main_vyper(halmos_options): +def test_main_vyper(dolmos_options): # vyper tests and vyper targets (requires the vyper compiler, used by forge) actual = dataclasses.asdict( - _main(["--root", "tests/vyper"] + halmos_options.split()) + _main(["--root", "tests/vyper"] + dolmos_options.split()) ) with open("tests/expected/vyper.json", encoding="utf8") as f: expected = json.load(f) @@ -81,8 +81,8 @@ def test_main_vyper(halmos_options): ], ids=("SetupFailTest",), ) -def test_main_fail(cmd, halmos_options): - actual = dataclasses.asdict(_main(cmd + halmos_options.split())) +def test_main_fail(cmd, dolmos_options): + actual = dataclasses.asdict(_main(cmd + dolmos_options.split())) assert actual["exitcode"] != 0 diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 502eb627..31d779b1 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -1,9 +1,9 @@ import pytest -from halmos.__main__ import mk_solver -from halmos.calldata import FunctionInfo -from halmos.config import default_config -from halmos.sevm import SEVM +from dolmos.__main__ import mk_solver +from dolmos.calldata import FunctionInfo +from dolmos.config import default_config +from dolmos.sevm import SEVM @pytest.fixture @@ -27,5 +27,5 @@ def solver(args): @pytest.fixture -def halmos_options(request): - return request.config.getoption("--halmos-options") +def dolmos_options(request): + return request.config.getoption("--dolmos-options") diff --git a/tests/test_mapper.py b/tests/test_mapper.py index 487a4e72..2e389adf 100644 --- a/tests/test_mapper.py +++ b/tests/test_mapper.py @@ -3,7 +3,7 @@ import pytest -from halmos.mapper import AstNode, ContractMappingInfo, Mapper, SingletonMeta +from dolmos.mapper import AstNode, ContractMappingInfo, Mapper, SingletonMeta @pytest.fixture diff --git a/tests/test_natspec.py b/tests/test_natspec.py new file mode 100644 index 00000000..099f7f43 --- /dev/null +++ b/tests/test_natspec.py @@ -0,0 +1,36 @@ +from dolmos.build import parse_devdoc, parse_natspec + + +def devdoc(**tags): + return {"metadata": {"output": {"devdoc": {"methods": {"f()": tags}}}}} + + +def test_parse_natspec_dolmos_tag(): + assert parse_natspec({"text": "@custom:dolmos --loop 4"}) == "--loop 4" + + +def test_parse_natspec_legacy_halmos_tag(): + # test suites written for halmos keep working + assert parse_natspec({"text": "@custom:halmos --loop 4"}) == "--loop 4" + + +def test_parse_natspec_both_tags(): + text = "@title T\n@custom:halmos --loop 4\n@notice n\n@custom:dolmos --width 2" + assert parse_natspec({"text": text}).split() == ["--loop", "4", "--width", "2"] + + +def test_parse_natspec_other_tags_ignored(): + assert parse_natspec({"text": "@custom:other --loop 4"}) == "" + + +def test_parse_devdoc(): + assert parse_devdoc("f()", devdoc(**{"custom:dolmos": "--loop 4"})) == "--loop 4" + assert parse_devdoc("f()", devdoc(**{"custom:halmos": "--loop 4"})) == "--loop 4" + assert ( + parse_devdoc( + "f()", devdoc(**{"custom:halmos": "--loop 4", "custom:dolmos": "--width 2"}) + ) + == "--width 2 --loop 4" + ) + assert parse_devdoc("f()", devdoc()) is None + assert parse_devdoc("g()", devdoc()) is None diff --git a/tests/test_prank.py b/tests/test_prank.py index ec5ee7ac..a7c2d45f 100644 --- a/tests/test_prank.py +++ b/tests/test_prank.py @@ -1,14 +1,14 @@ import pytest from z3 import BitVec -from halmos.cheatcodes import ( +from dolmos.cheatcodes import ( NO_PRANK, Prank, PrankResult, - halmos_cheat_code, hevm_cheat_code, + svm_cheat_code, ) -from halmos.sevm import CallContext, Message +from dolmos.sevm import CallContext, Message @pytest.fixture @@ -99,7 +99,7 @@ def test_lookup_no_active_prank(prank, other): # when we call lookup() without an active prank, it returns NO_PRANK assert prank.lookup(other) == NO_PRANK assert prank.lookup(hevm_cheat_code.address) == NO_PRANK - assert prank.lookup(halmos_cheat_code.address) == NO_PRANK + assert prank.lookup(svm_cheat_code.address) == NO_PRANK def test_prank_lookup(prank, sender, other): @@ -107,7 +107,7 @@ def test_prank_lookup(prank, sender, other): prank.prank(sender) # when calling lookup(to=) - for cheat_code in [hevm_cheat_code, halmos_cheat_code]: + for cheat_code in [hevm_cheat_code, svm_cheat_code]: result = prank.lookup(cheat_code.address) # then the active prank is ignored @@ -130,7 +130,7 @@ def test_startPrank_lookup(prank, sender, origin, other): prank.startPrank(sender, origin) # when calling lookup(to=) - for cheat_code in [hevm_cheat_code, halmos_cheat_code]: + for cheat_code in [hevm_cheat_code, svm_cheat_code]: result = prank.lookup(cheat_code.address) # then the active prank is ignored diff --git a/tests/test_sevm.py b/tests/test_sevm.py index ef05c2af..6a91b2dd 100644 --- a/tests/test_sevm.py +++ b/tests/test_sevm.py @@ -13,16 +13,16 @@ ZeroExt, ) -from halmos.__main__ import mk_block -from halmos.bitvec import HalmosBitVec as BV -from halmos.bytevec import ByteVec -from halmos.exceptions import ( +from dolmos.__main__ import mk_block +from dolmos.bitvec import DolmosBitVec as BV +from dolmos.bytevec import ByteVec +from dolmos.exceptions import ( InvalidJumpDestError, InvalidOpcode, OutOfGasError, StackUnderflowError, ) -from halmos.sevm import ( +from dolmos.sevm import ( SEVM, CallContext, Contract, @@ -40,7 +40,7 @@ uint160, uint256, ) -from halmos.utils import EVM +from dolmos.utils import EVM caller = BitVec("msg_sender", 160) origin = BitVec("tx_origin", 160) diff --git a/tests/test_solve.py b/tests/test_solve.py index 7f9d76cb..ecd419a5 100644 --- a/tests/test_solve.py +++ b/tests/test_solve.py @@ -1,12 +1,12 @@ import pytest -from halmos.solve import ModelVariable, parse_model_str +from dolmos.solve import ModelVariable, parse_model_str @pytest.mark.parametrize( "full_name", [ - "halmos_y_uint256_043cfd7_01", + "dolmos_y_uint256_043cfd7_01", "p_y_uint256_043cfd7_01", ], ) @@ -32,7 +32,7 @@ def test_smtlib_z3_bv_output(full_name): @pytest.mark.parametrize( "full_name", [ - "halmos_z_uint256_cabf047_02", + "dolmos_z_uint256_cabf047_02", "p_z_uint256_cabf047_02", ], ) @@ -58,7 +58,7 @@ def test_smtlib_yices_binary_output(full_name): @pytest.mark.parametrize( "full_name", [ - "halmos_z_uint256_11ce021_08", + "dolmos_z_uint256_11ce021_08", "p_z_uint256_11ce021_08", ], ) @@ -81,7 +81,7 @@ def test_smtlib_yices_decimal_output(full_name): @pytest.mark.parametrize( "full_name", [ - "halmos_x_uint8_043cfd7_01", + "dolmos_x_uint8_043cfd7_01", "p_x_uint8_043cfd7_01", ], ) diff --git a/tests/test_source_map.py b/tests/test_source_map.py index fd077267..ce0e0a96 100644 --- a/tests/test_source_map.py +++ b/tests/test_source_map.py @@ -3,7 +3,7 @@ import pytest -from halmos.mapper import SourceFileMap +from dolmos.mapper import SourceFileMap @pytest.fixture diff --git a/tests/test_traces.py b/tests/test_traces.py index d6dc22d5..3a88f972 100644 --- a/tests/test_traces.py +++ b/tests/test_traces.py @@ -13,11 +13,11 @@ is_bv, ) -import halmos.sevm -from halmos.__main__ import mk_block, render_trace -from halmos.bytevec import ByteVec -from halmos.exceptions import MessageDepthLimitError, Revert, WriteInStaticContext -from halmos.sevm import ( +import dolmos.sevm +from dolmos.__main__ import mk_block, render_trace +from dolmos.bytevec import ByteVec +from dolmos.exceptions import MessageDepthLimitError, Revert, WriteInStaticContext +from dolmos.sevm import ( SEVM, ZERO, CallContext, @@ -31,7 +31,7 @@ con, int_of, ) -from halmos.utils import EVM +from dolmos.utils import EVM # keccak256("FooEvent()") FOO_EVENT_SIG = 0x34E21A9428B1B47E73C4E509EABEEA7F2B74BECA07D82AAC87D4DD28B74C2A4A @@ -730,7 +730,7 @@ def test_static_context_propagates(sevm: SEVM, solver): assert next(inner_call.logs(), None) is None -def test_halmos_exception_halts_path(sevm: SEVM, solver): +def test_dolmos_exception_halts_path(sevm: SEVM, solver): _, runtime_hexcode = get_bytecode( """ contract Foo { @@ -853,7 +853,7 @@ def test_call_limit_with_create(monkeypatch, sevm: SEVM, solver): # 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) + monkeypatch.setattr(dolmos.sevm, "MAX_CALL_DEPTH", MAX_CALL_DEPTH_OVERRIDE) (is_single_exec, output_exec) = single(sevm.run(input_exec)) assert is_single_exec diff --git a/tests/test_utils.py b/tests/test_utils.py index 36e144d5..471c1612 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -12,8 +12,8 @@ simplify, ) -from halmos.solve import dirname -from halmos.utils import ( +from dolmos.solve import dirname +from dolmos.utils import ( f_sha3_256, match_dynamic_array_overflow_condition, ) diff --git a/tests/test_vyper.py b/tests/test_vyper.py index 98bf52c1..769fa8d2 100644 --- a/tests/test_vyper.py +++ b/tests/test_vyper.py @@ -2,8 +2,8 @@ import pytest -from halmos.__main__ import is_invalid_opcode_found, load_config, with_vyper_defaults -from halmos.build import ( +from dolmos.__main__ import is_invalid_opcode_found, load_config, with_vyper_defaults +from dolmos.build import ( VYPER_VERSION_PREFIX, build_out_view, build_output_iterator, @@ -16,14 +16,14 @@ parse_natspec, parse_vyper_docstrings, ) -from halmos.bytevec import ByteVec -from halmos.config import ConfigSource -from halmos.exceptions import InvalidOpcode, Revert +from dolmos.bytevec import ByteVec +from dolmos.config import ConfigSource +from dolmos.exceptions import InvalidOpcode, Revert VYPER_SOURCE = '''# pragma version ~=0.4.3 """ @title Test -@custom:halmos --loop 4 +@custom:dolmos --loop 4 """ counter: uint256 @@ -44,7 +44,7 @@ def check_multiline( y: DynArray[uint256, 3], ) -> uint256: # trailing comment \'\'\' - @custom:halmos --loop 5 + @custom:dolmos --loop 5 \'\'\' return x diff --git a/tests/vyper/foundry.toml b/tests/vyper/foundry.toml index a695c5de..6839b0d2 100644 --- a/tests/vyper/foundry.toml +++ b/tests/vyper/foundry.toml @@ -7,5 +7,5 @@ evm_version = 'cancun' force = false -# compile options used by halmos (to prevent unnecessary recompilation when running forge test and halmos together) +# compile options used by dolmos (to prevent unnecessary recompilation when running forge test and dolmos together) extra_output = ["storageLayout", "metadata"] diff --git a/tests/vyper/test/CounterTest.vy b/tests/vyper/test/CounterTest.vy index b3752059..426ff2eb 100644 --- a/tests/vyper/test/CounterTest.vy +++ b/tests/vyper/test/CounterTest.vy @@ -1,7 +1,7 @@ # pragma version ~=0.4.3 """ @title Symbolic tests written in Vyper -@custom:halmos --loop 3 +@custom:dolmos --loop 3 """ interface ICounter: @@ -66,6 +66,6 @@ def check_bytes_default(data: Bytes[100]): @external def check_bytes_annotated(data: Bytes[100]): """ - @custom:halmos --default-bytes-lengths 0,10 + @custom:dolmos --default-bytes-lengths 0,10 """ assert len(data) != 65, UNREACHABLE From 516f3a21330866c03b49877a18c92f8409438224 Mon Sep 17 00:00:00 2001 From: Hugo DE SA PEREIRA PINTO Date: Tue, 22 Sep 2026 16:23:37 +0200 Subject: [PATCH 19/46] commit correciton fichier docker --- packages/solvers/Dockerfile | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/solvers/Dockerfile b/packages/solvers/Dockerfile index 36a3e951..a359e40d 100644 --- a/packages/solvers/Dockerfile +++ b/packages/solvers/Dockerfile @@ -15,7 +15,8 @@ RUN apt-get update && apt-get install --no-install-recommends -y \ python3-venv \ ninja-build \ software-properties-common \ - g++ zlib1g-dev libboost-all-dev flex bison + g++ zlib1g-dev libboost-all-dev flex bison \ + pkg-config libgmp-dev libmpfr-dev # Install Z3 ARG Z3_VERSION=4.13.3 @@ -48,8 +49,11 @@ RUN wget https://github.com/cvc5/cvc5/releases/download/cvc5-${CVC5_VERSION}/cvc rm -rf /cvc5 # Install Bitwuzla +# pinned: its main branch changes without notice (since 0.9, GMP and MPFR must be +# installed on the system, found through pkg-config, and are linked statically) WORKDIR /bitwuzla -RUN git clone --depth 1 https://github.com/bitwuzla/bitwuzla . && \ +ARG BITWUZLA_VERSION=0.9.1 +RUN git clone --depth 1 --branch ${BITWUZLA_VERSION} https://github.com/bitwuzla/bitwuzla . && \ python3 -m venv .venv && \ . .venv/bin/activate && \ python3 -m pip install meson && \ @@ -84,6 +88,10 @@ COPY --from=builder \ /usr/local/bin/boolector \ /usr/local/bin/ +# Fail the build now rather than at test time if a solver misses a shared library +RUN ! ldd /usr/local/bin/* 2>/dev/null | grep "not found" && \ + bitwuzla --version && z3 --version + # Install uv COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ From 08fffdae6b5176b14b8cef189a5c485befec9f4c Mon Sep 17 00:00:00 2001 From: Hugo DE SA PEREIRA PINTO Date: Tue, 22 Sep 2026 16:33:22 +0200 Subject: [PATCH 20/46] Corrections fichiers workflows --- .github/workflows/copilot-setup-steps.yml | 2 +- .github/workflows/test-external.yml | 34 +++++++++++------------ .github/workflows/test-ffi.yml | 8 +++--- .github/workflows/test-long.yml | 12 ++++---- .github/workflows/test.yml | 16 +++++------ 5 files changed, 36 insertions(+), 36 deletions(-) diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index ad7e4d58..df870500 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -31,5 +31,5 @@ jobs: with: version: "0.5.21" - - name: Install halmos and its dependencies + - name: Install dolmos and its dependencies run: uv sync --extra dev diff --git a/.github/workflows/test-external.yml b/.github/workflows/test-external.yml index e0c9776b..6b75ba4c 100644 --- a/.github/workflows/test-external.yml +++ b/.github/workflows/test-external.yml @@ -5,8 +5,8 @@ on: branches: [main, chore-workflows] workflow_dispatch: inputs: - halmos-options: - description: "additional halmos options" + dolmos-options: + description: "additional dolmos options" required: false type: string default: "" @@ -77,40 +77,40 @@ jobs: profile: "" steps: - - name: Checkout halmos + - name: Checkout dolmos uses: actions/checkout@v4 with: # we won't be needing tests/lib for this workflow submodules: false - name: Build image - run: docker build -t halmos-image . --file packages/halmos/Dockerfile + run: docker build -t dolmos-image . --file packages/dolmos/Dockerfile - name: Run foundryup run: | - # run foundryup in halmos-image, save the result as a new image - docker run --name halmos-image-foundryup halmos-image foundryup + # run foundryup in dolmos-image, save the result as a new image + docker run --name dolmos-image-foundryup dolmos-image foundryup - # commit the result back to halmos-image - docker commit halmos-image-foundryup halmos-image + # commit the result back to dolmos-image + docker commit dolmos-image-foundryup dolmos-image # for snekmate - name: Install Vyper if: ${{ matrix.project.dir == 'snekmate' }} run: | - docker run --name halmos-image-vyper halmos-image uv pip install git+https://github.com/vyperlang/vyper@master - docker commit halmos-image-vyper halmos-image + docker run --name dolmos-image-vyper dolmos-image uv pip install git+https://github.com/vyperlang/vyper@master + docker commit dolmos-image-vyper dolmos-image # for sys-asm-halmos - name: Install Geas run: | - docker run --name tmp-halmos-image halmos-image bash -c "\ + docker run --name tmp-dolmos-image dolmos-image bash -c "\ apt-get update && \ apt-get install -y golang-go && \ go version && \ - env 'GOBIN=/halmos/bin' go install github.com/fjl/geas/cmd/geas@latest && \ + env 'GOBIN=/dolmos/bin' go install github.com/fjl/geas/cmd/geas@latest && \ geas -h" - docker commit tmp-halmos-image halmos-image + docker commit tmp-dolmos-image dolmos-image - name: Checkout external repo uses: actions/checkout@v4 @@ -121,13 +121,13 @@ jobs: submodules: recursive - name: Print forge version - run: docker run halmos-image forge --version + run: docker run dolmos-image forge --version - - name: Print halmos version - run: docker run halmos-image halmos --version + - name: Print dolmos version + run: docker run dolmos-image dolmos --version - name: Test external repo - run: docker run -e FOUNDRY_PROFILE -v .:/workspace halmos-image halmos ${{ matrix.project.cmd }} --statistics --solver-timeout-assertion 0 ${{ matrix.cache-solver }} ${{ inputs.halmos-options }} + run: docker run -e FOUNDRY_PROFILE -v .:/workspace dolmos-image dolmos ${{ matrix.project.cmd }} --statistics --solver-timeout-assertion 0 ${{ matrix.cache-solver }} ${{ inputs.dolmos-options }} working-directory: ${{ matrix.project.dir }} env: FOUNDRY_PROFILE: ${{ matrix.project.profile }} diff --git a/.github/workflows/test-ffi.yml b/.github/workflows/test-ffi.yml index 6026e5f6..e67f20ad 100644 --- a/.github/workflows/test-ffi.yml +++ b/.github/workflows/test-ffi.yml @@ -24,10 +24,10 @@ jobs: - name: Build image run: | - docker build -t halmos-image . --file packages/halmos/Dockerfile + docker build -t dolmos-image . --file packages/dolmos/Dockerfile - - name: Print halmos version - run: docker run halmos-image halmos --version + - name: Print dolmos version + run: docker run dolmos-image dolmos --version - name: Run pytest - run: docker run -v .:/workspace halmos-image pytest -v tests/test_halmos.py -k ${{ matrix.testname }} --halmos-options="--ffi -v -st" + run: docker run -v .:/workspace dolmos-image pytest -v tests/test_dolmos.py -k ${{ matrix.testname }} --dolmos-options="--ffi -v -st" diff --git a/.github/workflows/test-long.yml b/.github/workflows/test-long.yml index 03b99c34..f3452a66 100644 --- a/.github/workflows/test-long.yml +++ b/.github/workflows/test-long.yml @@ -5,8 +5,8 @@ on: branches: [main, chore-workflows] workflow_dispatch: inputs: - halmos-options: - description: "additional halmos options" + dolmos-options: + description: "additional dolmos options" required: false type: string default: "" @@ -34,11 +34,11 @@ jobs: - name: Build image run: | - docker build -t halmos-image . --file packages/halmos/Dockerfile + docker build -t dolmos-image . --file packages/dolmos/Dockerfile - - name: Print halmos version - run: docker run halmos-image halmos --version + - name: Print dolmos version + run: docker run dolmos-image dolmos --version - name: Run pytest run: | - docker run -v .:/workspace halmos-image pytest -x -v tests/test_halmos.py -k ${{ matrix.testname }} --halmos-options='-st --solver-threads 3 ${{ matrix.cache-solver }} ${{ inputs.halmos-options }}' -s --log-cli-level= + docker run -v .:/workspace dolmos-image pytest -x -v tests/test_dolmos.py -k ${{ matrix.testname }} --dolmos-options='-st --solver-threads 3 ${{ matrix.cache-solver }} ${{ inputs.dolmos-options }}' -s --log-cli-level= diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1145e13b..8a358083 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,8 +7,8 @@ on: branches: [main] workflow_dispatch: inputs: - halmos-options: - description: "additional halmos options" + dolmos-options: + description: "additional dolmos options" required: false type: string default: "" @@ -22,7 +22,7 @@ jobs: test: runs-on: ${{ matrix.os }} env: - HALMOS_ALLOW_DOWNLOAD: 1 + DOLMOS_ALLOW_DOWNLOAD: 1 strategy: fail-fast: false @@ -54,20 +54,20 @@ jobs: - name: Set up python ${{ matrix.python-version }} run: uv python install ${{ matrix.python-version }} - - name: Install halmos and its dependencies + - name: Install dolmos and its dependencies run: | uv sync --extra dev - - name: Write halmos.toml + - name: Write dolmos.toml shell: bash run: | - uv run python -m halmos.config \ + uv run python -m dolmos.config \ --storage-layout ${{ matrix.storage-layout }} \ ${{ matrix.cache-solver }} \ - | tee tests/regression/halmos.toml + | tee tests/regression/dolmos.toml - name: Run pytest shell: bash run: | uv run pytest -v -k "not long and not ffi" --ignore=tests/lib \ - --halmos-options="-st --disable-gc --solver-threads 1 ${{ inputs.halmos-options }}" ${{ inputs.pytest-options }} + --dolmos-options="-st --disable-gc --solver-threads 1 ${{ inputs.dolmos-options }}" ${{ inputs.pytest-options }} From 4f9287145fcccafe971769a4805832e023fb89d7 Mon Sep 17 00:00:00 2001 From: Hugo DE SA PEREIRA PINTO Date: Tue, 22 Sep 2026 17:56:26 +0200 Subject: [PATCH 21/46] Update Workflows --- .devcontainer/postCreate.sh | 2 +- .../publish-dolmos-builder-package.yml | 62 +++++++++++++++++ .github/workflows/publish-dolmos-package.yml | 68 +++++++++++++++++++ .github/workflows/publish-solvers-package.yml | 2 +- 4 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/publish-dolmos-builder-package.yml create mode 100644 .github/workflows/publish-dolmos-package.yml diff --git a/.devcontainer/postCreate.sh b/.devcontainer/postCreate.sh index 85a31ea8..0e4252fc 100644 --- a/.devcontainer/postCreate.sh +++ b/.devcontainer/postCreate.sh @@ -14,4 +14,4 @@ curl -L https://foundry.paradigm.xyz | bash echo 'source .venv/bin/activate' >> ~/.bashrc # just a test -# pytest -v -k "not long and not ffi" --ignore=tests/lib --halmos-options="-v -st --storage-layout solidity --solver-timeout-assertion 0" +# pytest -v -k "not long and not ffi" --ignore=tests/lib --dolmos-options="-v -st --storage-layout solidity --solver-timeout-assertion 0" diff --git a/.github/workflows/publish-dolmos-builder-package.yml b/.github/workflows/publish-dolmos-builder-package.yml new file mode 100644 index 00000000..5314a5cc --- /dev/null +++ b/.github/workflows/publish-dolmos-builder-package.yml @@ -0,0 +1,62 @@ +# based on https://docs.github.com/en/packages/managing-github-packages-using-github-actions-workflows/publishing-and-installing-a-package-with-github-actions#upgrading-a-workflow-that-accesses-a-registry-using-a-personal-access-token +name: Push dolmos-builder package + +on: + # only trigger manually, this is independent from dolmos releases + workflow_dispatch: + workflow_run: + workflows: ["Push solvers package"] + types: + - completed + push: + paths: + - packages/dolmos-builder/**/* + - .github/workflows/publish-dolmos-builder-package.yml + +env: + IMAGE_NAME: dolmos-builder + +jobs: + # This pushes the image to GitHub Packages. + push: + # if triggered by a workflow_run, only run if the previous workflow succeeded + if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name != 'workflow_run' }} + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + + steps: + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: actions/checkout@v4 + with: + submodules: false + + - name: Build image + run: docker build . --file packages/dolmos-builder/Dockerfile --tag $IMAGE_NAME --label "runnumber=${GITHUB_RUN_ID}" + + - name: Push image + run: | + IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME + + # This changes all uppercase characters to lowercase. + IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') + + # This strips the git ref prefix from the version. + VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') + + # This strips the "v" prefix from the tag name. + [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') + + # This uses the Docker `latest` tag convention. + [ "$VERSION" == "main" ] && VERSION=latest + echo IMAGE_ID=$IMAGE_ID + echo VERSION=$VERSION + docker tag $IMAGE_NAME $IMAGE_ID:$VERSION + docker push $IMAGE_ID:$VERSION diff --git a/.github/workflows/publish-dolmos-package.yml b/.github/workflows/publish-dolmos-package.yml new file mode 100644 index 00000000..853c4690 --- /dev/null +++ b/.github/workflows/publish-dolmos-package.yml @@ -0,0 +1,68 @@ +# based on https://docs.github.com/en/packages/managing-github-packages-using-github-actions-workflows/publishing-and-installing-a-package-with-github-actions#upgrading-a-workflow-that-accesses-a-registry-using-a-personal-access-token +name: Push dolmos package + +on: + workflow_dispatch: + workflow_run: + workflows: ["Push dolmos-builder package"] + types: + - completed + push: + paths: + - packages/dolmos/**/* + - .github/workflows/publish-dolmos-package.yml + tags: + # push when we release a new tagged version + - 'v*' + +env: + IMAGE_NAME: dolmos + +jobs: + # This pushes the image to GitHub Packages. + push: + # if triggered by a workflow_run, only run if the previous workflow succeeded + if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name != 'workflow_run' }} + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + + steps: + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: actions/checkout@v4 + with: + # we may need tests/lib for the workflows using this image + submodules: recursive + + - name: Build image + run: docker build . --file packages/dolmos/Dockerfile --tag $IMAGE_NAME --label "runnumber=${GITHUB_RUN_ID}" + + - name: Print dolmos version + run: docker run $IMAGE_NAME dolmos --version + + - name: Push image + run: | + IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME + + # This changes all uppercase characters to lowercase. + IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') + + # This strips the git ref prefix from the version. + VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') + + # This strips the "v" prefix from the tag name. + [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') + + # This uses the Docker `latest` tag convention. + [ "$VERSION" == "main" ] && VERSION=latest + echo IMAGE_ID=$IMAGE_ID + echo VERSION=$VERSION + docker tag $IMAGE_NAME $IMAGE_ID:$VERSION + docker push $IMAGE_ID:$VERSION diff --git a/.github/workflows/publish-solvers-package.yml b/.github/workflows/publish-solvers-package.yml index b1274e14..a7b03049 100644 --- a/.github/workflows/publish-solvers-package.yml +++ b/.github/workflows/publish-solvers-package.yml @@ -2,7 +2,7 @@ name: Push solvers package on: - # only trigger manually, this is independent from halmos releases + # only trigger manually, this is independent from dolmos releases workflow_dispatch: push: paths: From 8290224bc7c76244ea3a905f7b90f0a0fd4f9a5f Mon Sep 17 00:00:00 2001 From: Hugo DE SA PEREIRA PINTO Date: Tue, 22 Sep 2026 18:01:44 +0200 Subject: [PATCH 22/46] =?UTF-8?q?Modification=20du=20num=C3=A9ro=20de=20ve?= =?UTF-8?q?rsion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/dolmos/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dolmos/Dockerfile b/packages/dolmos/Dockerfile index 3e041bb8..f92e7648 100644 --- a/packages/dolmos/Dockerfile +++ b/packages/dolmos/Dockerfile @@ -5,13 +5,13 @@ FROM ghcr.io/dowsers/dolmos-builder:latest # - enable the virtual environment # - install dolmos and its dependencies in UV_PROJECT_ENVIRONMENT # - enable bytecode compilation for faster startup -# - disable downloading any additional packages +# - use the python of the builder image (ubuntu 24.04 ships 3.12), never download one # - use copy mode for linking instead of symlinking (because we mount to /src temporarily) ENV PATH="/dolmos/bin:$PATH" \ VIRTUAL_ENV='/dolmos' \ UV_PROJECT_ENVIRONMENT='/dolmos' \ UV_COMPILE_BYTECODE=1 \ - UV_PYTHON=python3.13 \ + UV_PYTHON=python3.12 \ UV_PYTHON_DOWNLOADS=never \ UV_LINK_MODE=copy From 4254625b18d0d19ecf9f46e43c4f27d3aeb06a67 Mon Sep 17 00:00:00 2001 From: Hugo DE SA PEREIRA PINTO Date: Tue, 22 Sep 2026 18:50:27 +0200 Subject: [PATCH 23/46] Handling windows- POSIX --- src/dolmos/build.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/dolmos/build.py b/src/dolmos/build.py index ed4ec2e0..7426990b 100644 --- a/src/dolmos/build.py +++ b/src/dolmos/build.py @@ -2,6 +2,7 @@ import json import os +import posixpath import re import traceback @@ -50,6 +51,11 @@ def get_source_path(contract_json: dict, default: str | None = None) -> str | No return default +def _artifact_key(path: str) -> str: + """Normalize an artifact path to a platform-independent key (`/` separators).""" + return posixpath.normpath(path.replace("\\", "/")) + + def load_forge_cache(root: str, out_path: str) -> dict: """ Return a mapping from artifact path (relative to the build output directory, @@ -77,11 +83,11 @@ def load_forge_cache(root: str, out_path: str) -> dict: if not artifact_path: continue # paths are relative to the out dir; tolerate an `out/` prefix - artifact_path = os.path.normpath(artifact_path) - out_prefix = os.path.basename(os.path.normpath(out_path)) + os.sep + artifact_path = _artifact_key(artifact_path) + out_prefix = os.path.basename(os.path.normpath(out_path)) + "/" if artifact_path.startswith(out_prefix): artifact_path = artifact_path[len(out_prefix) :] - result[os.path.normpath(artifact_path)] = (source_path, version) + result[artifact_path] = (source_path, version) return result @@ -247,7 +253,7 @@ def parse_build_out(args: DolmosConfig) -> dict: forge_cache = load_forge_cache(root, out_path) cache_entry = forge_cache.get( - os.path.normpath(os.path.join(sol_dirname, json_filename)) + _artifact_key(f"{sol_dirname}/{json_filename}") ) contract_type, compiler_version, natspec = normalize_vyper_artifact( json_out, sol_dirname, root, cache_entry From 2f07201c05d5b3e39c94f952449bcfbabb3cbea2 Mon Sep 17 00:00:00 2001 From: Hugo DE SA PEREIRA PINTO Date: Tue, 22 Sep 2026 19:17:45 +0200 Subject: [PATCH 24/46] CI : handling last foundry version --- src/dolmos/__main__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/dolmos/__main__.py b/src/dolmos/__main__.py index ae4c6c86..8083432e 100644 --- a/src/dolmos/__main__.py +++ b/src/dolmos/__main__.py @@ -1870,7 +1870,14 @@ def _main(_args=None) -> MainResult: # run forge without capturing stdout/stderr debug(f"Running {' '.join(build_cmd)}") - build_exitcode = subprocess.run(build_cmd).returncode + # recent forge versions enable `dynamic_test_linking` by default, which rewrites + # `new C(...)` in test files into `vm.deployCode(...)` calls. dolmos needs the + # original creation bytecode, so we turn it off (env var: ignored by older forge, + # and still overridable by the user). + build_env = os.environ.copy() + build_env.setdefault("FOUNDRY_DYNAMIC_TEST_LINKING", "false") + + build_exitcode = subprocess.run(build_cmd, env=build_env).returncode if build_exitcode: error(f"Build failed: {build_cmd}") From 4301f76805c9e24cc71e343eab4dfebd871fccb3 Mon Sep 17 00:00:00 2001 From: Hugo DE SA PEREIRA PINTO Date: Tue, 22 Sep 2026 19:29:26 +0200 Subject: [PATCH 25/46] CI: explicit solady remapping for recent forge auto-remapping --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- .../publish-halmos-builder-package.yml | 62 ----------------- .github/workflows/publish-halmos-package.yml | 68 ------------------- examples/tokens/ERC20/remappings.txt | 1 + examples/tokens/ERC721/remappings.txt | 1 + 5 files changed, 3 insertions(+), 131 deletions(-) delete mode 100644 .github/workflows/publish-halmos-builder-package.yml delete mode 100644 .github/workflows/publish-halmos-package.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 58b11868..8343d9a2 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -16,7 +16,7 @@ Please provide a reproducible example. **Environment:** - OS: [e.g., Linux / Windows / macOS] - Python version: [e.g., `python --version`] - - Halmos and other dependency versions: [e.g., `pip list`] + - dolmos and other dependency versions: [e.g., `pip list`] **Additional context** Add any other context about the problem here. diff --git a/.github/workflows/publish-halmos-builder-package.yml b/.github/workflows/publish-halmos-builder-package.yml deleted file mode 100644 index 60a6a494..00000000 --- a/.github/workflows/publish-halmos-builder-package.yml +++ /dev/null @@ -1,62 +0,0 @@ -# based on https://docs.github.com/en/packages/managing-github-packages-using-github-actions-workflows/publishing-and-installing-a-package-with-github-actions#upgrading-a-workflow-that-accesses-a-registry-using-a-personal-access-token -name: Push halmos-builder package - -on: - # only trigger manually, this is independent from halmos releases - workflow_dispatch: - workflow_run: - workflows: ["Push solvers package"] - types: - - completed - push: - paths: - - packages/halmos-builder/**/* - - .github/workflows/publish-halmos-builder-package.yml - -env: - IMAGE_NAME: halmos-builder - -jobs: - # This pushes the image to GitHub Packages. - push: - # if triggered by a workflow_run, only run if the previous workflow succeeded - if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name != 'workflow_run' }} - runs-on: ubuntu-latest - permissions: - packages: write - contents: read - - steps: - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - uses: actions/checkout@v4 - with: - submodules: false - - - name: Build image - run: docker build . --file packages/halmos-builder/Dockerfile --tag $IMAGE_NAME --label "runnumber=${GITHUB_RUN_ID}" - - - name: Push image - run: | - IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME - - # This changes all uppercase characters to lowercase. - IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') - - # This strips the git ref prefix from the version. - VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') - - # This strips the "v" prefix from the tag name. - [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') - - # This uses the Docker `latest` tag convention. - [ "$VERSION" == "main" ] && VERSION=latest - echo IMAGE_ID=$IMAGE_ID - echo VERSION=$VERSION - docker tag $IMAGE_NAME $IMAGE_ID:$VERSION - docker push $IMAGE_ID:$VERSION diff --git a/.github/workflows/publish-halmos-package.yml b/.github/workflows/publish-halmos-package.yml deleted file mode 100644 index abbef399..00000000 --- a/.github/workflows/publish-halmos-package.yml +++ /dev/null @@ -1,68 +0,0 @@ -# based on https://docs.github.com/en/packages/managing-github-packages-using-github-actions-workflows/publishing-and-installing-a-package-with-github-actions#upgrading-a-workflow-that-accesses-a-registry-using-a-personal-access-token -name: Push halmos package - -on: - workflow_dispatch: - workflow_run: - workflows: ["Push halmos-builder package"] - types: - - completed - push: - paths: - - packages/halmos/**/* - - .github/workflows/publish-halmos-package.yml - tags: - # push when we release a new tagged version - - 'v*' - -env: - IMAGE_NAME: halmos - -jobs: - # This pushes the image to GitHub Packages. - push: - # if triggered by a workflow_run, only run if the previous workflow succeeded - if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name != 'workflow_run' }} - runs-on: ubuntu-latest - permissions: - packages: write - contents: read - - steps: - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - uses: actions/checkout@v4 - with: - # we may need tests/lib for the workflows using this image - submodules: recursive - - - name: Build image - run: docker build . --file packages/halmos/Dockerfile --tag $IMAGE_NAME --label "runnumber=${GITHUB_RUN_ID}" - - - name: Print halmos version - run: docker run $IMAGE_NAME halmos --version - - - name: Push image - run: | - IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME - - # This changes all uppercase characters to lowercase. - IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') - - # This strips the git ref prefix from the version. - VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') - - # This strips the "v" prefix from the tag name. - [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') - - # This uses the Docker `latest` tag convention. - [ "$VERSION" == "main" ] && VERSION=latest - echo IMAGE_ID=$IMAGE_ID - echo VERSION=$VERSION - docker tag $IMAGE_NAME $IMAGE_ID:$VERSION - docker push $IMAGE_ID:$VERSION diff --git a/examples/tokens/ERC20/remappings.txt b/examples/tokens/ERC20/remappings.txt index acf3e02d..95f579e0 100644 --- a/examples/tokens/ERC20/remappings.txt +++ b/examples/tokens/ERC20/remappings.txt @@ -1,2 +1,3 @@ openzeppelin/=../../../tests/lib/openzeppelin-contracts/contracts/ ds-test/=../../../tests/lib/forge-std/lib/ds-test/src/ +solady/=../../../tests/lib/solady/src/ diff --git a/examples/tokens/ERC721/remappings.txt b/examples/tokens/ERC721/remappings.txt index acf3e02d..95f579e0 100644 --- a/examples/tokens/ERC721/remappings.txt +++ b/examples/tokens/ERC721/remappings.txt @@ -1,2 +1,3 @@ openzeppelin/=../../../tests/lib/openzeppelin-contracts/contracts/ ds-test/=../../../tests/lib/forge-std/lib/ds-test/src/ +solady/=../../../tests/lib/solady/src/ From 0c2296535db76841f5c4ac2321b3dba458ea609c Mon Sep 17 00:00:00 2001 From: Hugo DE SA PEREIRA PINTO Date: Tue, 22 Sep 2026 21:08:18 +0200 Subject: [PATCH 26/46] Adding changelog and modyfiing readme --- CHANGELOG.md | 41 +++++++++++++++++++++++++++++++ README.md | 68 +++++++++++++++++++++++++++++++++------------------- RELEASING.md | 60 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 144 insertions(+), 25 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 RELEASING.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..3697ff1a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,41 @@ +# Changelog + +All notable changes to dolmos are documented here. The format follows +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and versions follow +[Semantic Versioning](https://semver.org/). Dolmos is derived from halmos; see +[NOTICE](NOTICE) for the upstream commit it is based on. + +## [Unreleased] + +## [0.1.0] - YYYY-MM-DD + +First release of dolmos. + +### Added + +- CREATE address derivation from `keccak256(rlp([sender, nonce]))` and account + nonces (EIP-161), so that CREATE3-style address prediction matches execution +- nonce cheatcodes: `vm.getNonce`, `vm.setNonce`, `vm.setNonceUnsafe`, + `vm.resetNonce` +- opcodes `CLZ` (EIP-7939), `BLOBHASH` and `BLOBBASEFEE` (EIP-4844) +- experimental Vyper support: Vyper test contracts, Solidity tests targeting + Vyper contracts, `@custom:dolmos` annotations in docstrings, + `--invalid-as-failure` +- `dolmos.toml` configuration file and `@custom:dolmos` annotations + (`halmos.toml`, `@custom:halmos` and `HALMOS_ALLOW_DOWNLOAD` remain accepted) +- PyPI package and Docker image `ghcr.io/dowsers/dolmos` + +### Changed + +- command, Python package and symbolic variable prefix renamed from `halmos` to + `dolmos`; downloaded solvers are cached in `~/.dolmos/solvers` + +### Fixed + +- Vyper artifact lookup in the forge cache on Windows +- builds with forge ≥ 1.8: `dynamic_test_linking` is disabled when dolmos runs + `forge build`, since it rewrites `new C()` into unsupported `vm.deployCode` + calls + +[Unreleased]: https://github.com/Dowsers/dolmos/compare/v0.1.0...HEAD +[0.1.0]: https://github.com/Dowsers/dolmos/releases/tag/v0.1.0 diff --git a/README.md b/README.md index 36087a3b..0c758f41 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Dolmos -[![License](https://img.shields.io/github/license/Dowsers/dolmos)](LICENSE) +[![License](https://img.shields.io/github/license/Dowsers/dolmos)](https://github.com/Dowsers/dolmos/blob/main/LICENSE) [![Python Version from PEP 621 TOML](https://img.shields.io/python/required-version-toml?tomlFilePath=https%3A%2F%2Fraw.githubusercontent.com%2FDowsers%2Fdolmos%2Frefs%2Fheads%2Fmain%2Fpyproject.toml)](pyproject.toml) Dolmos is a _symbolic testing_ tool for EVM smart contracts, driven by @@ -9,8 +9,8 @@ audit and formal verification work. Tests look like fuzz tests, but inputs are symbolic: a passing test holds for every input, and a failing one comes with a counterexample. -Start with the [getting started guide](docs/getting-started.md) and the -[examples](examples/README.md). +Start with the [getting started guide](https://github.com/Dowsers/dolmos/blob/main/docs/getting-started.md) and the +[examples](https://github.com/Dowsers/dolmos/blob/main/examples/README.md). ## Features @@ -56,7 +56,7 @@ emits no source maps for it. See `tests/vyper` for examples. ## Compatibility with halmos -Dolmos is derived from halmos (see [NOTICE](NOTICE)) and accepts the names used +Dolmos is derived from halmos (see [NOTICE](https://github.com/Dowsers/dolmos/blob/main/NOTICE)) and accepts the names used by test suites written for it, so they should run without changes: | halmos | dolmos | | @@ -72,30 +72,49 @@ Some things do change: the command and the Python package are named `dolmos` ## Installation -Requires Python ≥ 3.11 and [Foundry](https://getfoundry.sh). Dolmos is not -published on PyPI and has no prebuilt binaries or published Docker image yet, so -install it from source: +Requires Python ≥ 3.11 and [Foundry](https://getfoundry.sh). + +With [uv](https://docs.astral.sh/uv/) (recommended, installs `dolmos` as an +isolated command-line tool): ```sh -git clone https://github.com/Dowsers/dolmos.git -cd dolmos -python3.12 -m venv .venv && source .venv/bin/activate -pip install -e . +uv tool install dolmos dolmos --version ``` -Development install, with the test and lint dependencies: +With pipx or pip: ```sh -pip install -e ".[dev]" -pre-commit install +pipx install dolmos +# or, in a virtual environment +pip install dolmos +``` + +Upgrade with `uv tool upgrade dolmos` (or `pipx upgrade dolmos`, +`pip install -U dolmos`). + +### Docker + +Each release is published as a Docker image that bundles dolmos, Foundry and the +SMT solvers (yices, z3, cvc5, bitwuzla, stp): + +```sh +docker run --rm -v "$PWD":/workspace ghcr.io/dowsers/dolmos:latest dolmos +# or a fixed version +docker run --rm -v "$PWD":/workspace ghcr.io/dowsers/dolmos:0.1.0 dolmos +``` + +### From source + +```sh +git clone https://github.com/Dowsers/dolmos.git +cd dolmos +uv sync --extra dev # or: python -m venv .venv && pip install -e ".[dev]" +uv run dolmos --version ``` -The Docker images are built by the `publish-solvers-package`, -`publish-dolmos-builder-package` and `publish-dolmos-package` workflows, in that -order, and pushed to `ghcr.io//`. The workflows that run tests inside -those images (`test-ffi`, `test-long`, `test-external`) need them to be -published first. +See [CONTRIBUTING.md](https://github.com/Dowsers/dolmos/blob/main/CONTRIBUTING.md) +for the development setup. ## Usage @@ -119,7 +138,7 @@ solver-timeout-assertion = 10000 ``` `python -m dolmos.config` prints every option with its default value, in that -format. Warnings link to their explanation in [docs/warnings.md](docs/warnings.md). +format. Warnings link to their explanation in [docs/warnings.md](https://github.com/Dowsers/dolmos/blob/main/docs/warnings.md). ## Tests @@ -151,7 +170,7 @@ A few notes on the toolchain: ## Contributing -See [CONTRIBUTING.md](CONTRIBUTING.md). +See [CONTRIBUTING.md](https://github.com/Dowsers/dolmos/blob/main/CONTRIBUTING.md). ## Roadmap @@ -161,12 +180,11 @@ See [CONTRIBUTING.md](CONTRIBUTING.md). matches, bound the concurrency of solver subprocesses, memoize invariant target resolution - support multiple `setUp()` states -- a published package and Docker image ## License -AGPL-3.0, see [LICENSE](LICENSE). Dolmos is a modified version of another -AGPL-3.0 program; [NOTICE](NOTICE) gives its origin and the notices that come +AGPL-3.0, see [LICENSE](https://github.com/Dowsers/dolmos/blob/main/LICENSE). Dolmos is a modified version of another +AGPL-3.0 program; [NOTICE](https://github.com/Dowsers/dolmos/blob/main/NOTICE) gives its origin and the notices that come with it. ## Disclaimer @@ -176,5 +194,5 @@ without warranty of any kind, express or implied, including any warranty of merchantability, non-infringement or fitness for a particular purpose. They have not been audited. Passing symbolic tests is not a proof that a contract is free of bugs. Nothing in this repository is investment or legal advice. See -[NOTICE](NOTICE) for the disclaimers attached to the original work this code is +[NOTICE](https://github.com/Dowsers/dolmos/blob/main/NOTICE) for the disclaimers attached to the original work this code is derived from._ diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 00000000..f8b6215e --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,60 @@ +# Releasing dolmos + +A release is a git tag `vX.Y.Z` plus a GitHub Release. The package version is +derived from the tag by setuptools-scm: there is no version number to edit in +the code. + +## One-time setup + +1. **PyPI Trusted Publisher.** On https://pypi.org/manage/account/publishing/, + add a *pending publisher* (the `dolmos` project does not exist yet): + owner `Dowsers`, repository `dolmos`, workflow `publish-pypi.yml`, + environment `release`. After the first upload, the pending publisher becomes + a regular one. Do the same on https://test.pypi.org if you want dry runs. +2. **GitHub environment.** In the repository settings, create the `release` + environment and add required reviewers, so that a PyPI upload always needs + an explicit approval. +3. **Docker images.** Run, in this order and once, the workflows + `Push solvers package`, `Push dolmos-builder package` and + `Push dolmos package` (each triggers the next one). Then, in the + organization's *Packages* page, set `solvers`, `dolmos-builder` and `dolmos` + to **public** and link them to this repository. The CI jobs that run inside + these images (`test-long`, `test-ffi`, `test-external`) need them. +4. **Tag protection.** Add a ruleset that restricts who can create `v*` tags. + +## Release checklist + +1. All workflows are green on `main` (`Test`, `Test long`, `Test external`, + `pre-commit`). +2. Move the entries of `## [Unreleased]` in `CHANGELOG.md` under a new + `## [X.Y.Z] - YYYY-MM-DD` heading, update the links at the bottom, commit. +3. Optional dry run: run `Publish to PyPI` by hand (*workflow_dispatch*); it + builds, checks and smoke-tests the distributions without publishing. +4. Tag and push: + + ```sh + git tag -a vX.Y.Z -m "dolmos X.Y.Z" + git push origin vX.Y.Z + ``` + + The tag push triggers `Push dolmos package`, which publishes + `ghcr.io/dowsers/dolmos:X.Y.Z`, `:X.Y` and `:latest`. +5. Create the GitHub Release from the tag, with the changelog section as notes: + + ```sh + gh release create vX.Y.Z --title "dolmos X.Y.Z" --notes-file <(sed -n '/## \[X.Y.Z\]/,/## \[/p' CHANGELOG.md | sed '$d') + ``` + + Publishing it triggers `Publish to PyPI` (after the `release` environment + approval), which uploads to PyPI and attaches the wheel and sdist to the + release. +6. Check: `uv tool install dolmos==X.Y.Z && dolmos --version`, and + `docker run --rm ghcr.io/dowsers/dolmos:X.Y.Z dolmos --version`. + +A version published on PyPI can never be re-uploaded. If a release is broken, +yank it on PyPI and release `X.Y.(Z+1)`. + +## Version numbers + +- `0.x`: the CLI and the configuration may still change between minor versions. +- Patch releases (`0.1.1`) only contain fixes. From 9d7bc81d4066f67f4c7e2466ddf01fde5e9410e1 Mon Sep 17 00:00:00 2001 From: Hugo DE SA PEREIRA PINTO Date: Tue, 22 Sep 2026 21:20:10 +0200 Subject: [PATCH 27/46] Pre release --- CHANGELOG.md | 2 +- MANIFEST.in | 6 ++++++ packages/dolmos-builder/Dockerfile | 6 ++++-- pyproject.toml | 20 +++++++++++++++++--- 4 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 MANIFEST.in diff --git a/CHANGELOG.md b/CHANGELOG.md index 3697ff1a..f6e37996 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ All notable changes to dolmos are documented here. The format follows ## [Unreleased] -## [0.1.0] - YYYY-MM-DD +## [0.1.0] - 2026-09-22 First release of dolmos. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..3d8cb64a --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,6 @@ +# setuptools-scm puts every git-tracked file in the sdist, submodules included. +# Keep the sdist to what is needed to build and test dolmos itself. +prune tests/lib +prune benchmarks +prune .github +prune .devcontainer diff --git a/packages/dolmos-builder/Dockerfile b/packages/dolmos-builder/Dockerfile index ac6d991f..3e957761 100644 --- a/packages/dolmos-builder/Dockerfile +++ b/packages/dolmos-builder/Dockerfile @@ -18,11 +18,13 @@ RUN mkdir -p /root/.foundry/bin /root/.foundry/share/man/man1 # Add foundry binaries to PATH ENV PATH="/root/.foundry/bin:${PATH}" -# Download and run foundryup +# Download foundryup and install a pinned foundry version, so that the images (and +# the CI jobs that use them) do not change behaviour when foundry releases +ARG FOUNDRY_VERSION=v1.8.3 RUN curl -sSf -L https://raw.githubusercontent.com/foundry-rs/foundry/master/foundryup/foundryup \ -o /root/.foundry/bin/foundryup && \ chmod +x /root/.foundry/bin/foundryup && \ - foundryup + foundryup --install ${FOUNDRY_VERSION} # Set a nicer prompt ENV IMAGE_NAME=dolmos-builder diff --git a/pyproject.toml b/pyproject.toml index b8fc193f..a88318a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,17 @@ [build-system] -requires = ["setuptools>=61.0", "setuptools_scm[toml]>=6.2"] +requires = ["setuptools>=77.0", "setuptools_scm[toml]>=8.0"] build-backend = "setuptools.build_meta" [tool.setuptools_scm] +# the version comes from the git tag (vX.Y.Z); a missing tag gives 0.1.devN [project] name = "dolmos" description = "A symbolic testing tool for EVM smart contracts" readme = "README.md" +license = "AGPL-3.0-only" +license-files = ["LICENSE", "NOTICE"] +keywords = ["ethereum", "evm", "solidity", "vyper", "symbolic-execution", "formal-verification", "foundry"] # dolmos is derived from other authors' work, see NOTICE authors = [ { name="Dowsers SAS" }, @@ -16,9 +20,16 @@ maintainers = [ { name="Dowsers SAS" }, ] classifiers = [ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: GNU Affero General Public License v3", + "Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: Developers", "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Security", + "Topic :: Software Development :: Testing", ] requires-python = ">=3.11" dependencies = [ @@ -40,6 +51,9 @@ dolmos = "dolmos.__main__:main" [project.urls] "Homepage" = "https://github.com/Dowsers/dolmos" +"Documentation" = "https://github.com/Dowsers/dolmos/blob/main/docs/getting-started.md" +"Issues" = "https://github.com/Dowsers/dolmos/issues" +"Changelog" = "https://github.com/Dowsers/dolmos/blob/main/CHANGELOG.md" # development dependencies, can be installed with # uv sync --extra dev From ab91df8037d0b0e0f254c5514cb51d04a386550f Mon Sep 17 00:00:00 2001 From: Hugo DE SA PEREIRA PINTO Date: Tue, 22 Sep 2026 21:31:18 +0200 Subject: [PATCH 28/46] Workflows --- .github/workflows/publish-dolmos-package.yml | 24 +++--- .github/workflows/publish-pypi.yml | 79 ++++++++++++++++++-- .github/workflows/test-external.yml | 6 +- 3 files changed, 89 insertions(+), 20 deletions(-) diff --git a/.github/workflows/publish-dolmos-package.yml b/.github/workflows/publish-dolmos-package.yml index 853c4690..f4ed9990 100644 --- a/.github/workflows/publish-dolmos-package.yml +++ b/.github/workflows/publish-dolmos-package.yml @@ -54,15 +54,17 @@ jobs: # This changes all uppercase characters to lowercase. IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') - # This strips the git ref prefix from the version. - VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') + if [[ "${{ github.ref }}" == refs/tags/v* ]]; then + # release: X.Y.Z, X.Y and latest + VERSION="${GITHUB_REF_NAME#v}" + TAGS="$VERSION ${VERSION%.*} latest" + else + # anything else (main, builder rebuilds): a moving `main` tag + TAGS="main" + fi - # This strips the "v" prefix from the tag name. - [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') - - # This uses the Docker `latest` tag convention. - [ "$VERSION" == "main" ] && VERSION=latest - echo IMAGE_ID=$IMAGE_ID - echo VERSION=$VERSION - docker tag $IMAGE_NAME $IMAGE_ID:$VERSION - docker push $IMAGE_ID:$VERSION + for TAG in $TAGS; do + echo "pushing $IMAGE_ID:$TAG" + docker tag $IMAGE_NAME $IMAGE_ID:$TAG + docker push $IMAGE_ID:$TAG + done diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 0984e529..3f37dab2 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -1,22 +1,87 @@ name: Publish to PyPI + +# Triggered by publishing a GitHub Release whose tag is vX.Y.Z. +# The package version comes from that tag (setuptools-scm). + on: release: types: [published] + # manual dry run: builds and checks the distributions, publishes nothing + workflow_dispatch: + jobs: - pypi: + build: runs-on: ubuntu-latest - environment: release - permissions: - # IMPORTANT: this permission is mandatory for Trusted Publishing - # https://docs.pypi.org/trusted-publishers/using-a-publisher/ - id-token: write steps: - name: Checkout uses: actions/checkout@v4 with: + # setuptools-scm needs the tags and the history fetch-depth: 0 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Build - run: python3 -m pip install --upgrade build && python3 -m build + run: | + python -m pip install --upgrade build twine + python -m build + twine check --strict dist/* + + - name: Check that the version matches the tag + if: github.event_name == 'release' + run: | + expected="${GITHUB_REF_NAME#v}" + wheel=$(ls dist/*.whl) + case "$wheel" in + dist/dolmos-${expected}-*) echo "version $expected OK" ;; + *) echo "::error::wheel $wheel does not match tag $GITHUB_REF_NAME"; exit 1 ;; + esac + + - name: Smoke test the wheel + run: | + python -m venv /tmp/smoke + /tmp/smoke/bin/pip install dist/*.whl + /tmp/smoke/bin/dolmos --version + /tmp/smoke/bin/python -Ic 'import dolmos' + + - uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + pypi: + needs: build + if: github.event_name == 'release' + runs-on: ubuntu-latest + environment: + name: release + url: https://pypi.org/project/dolmos/ + permissions: + # mandatory for Trusted Publishing + # https://docs.pypi.org/trusted-publishers/using-a-publisher/ + id-token: write + steps: + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ - name: Publish uses: pypa/gh-action-pypi-publish@release/v1 + github-release-assets: + needs: build + if: github.event_name == 'release' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + - name: Attach the distributions to the GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: gh release upload "$GITHUB_REF_NAME" dist/* --repo "$GITHUB_REPOSITORY" --clobber diff --git a/.github/workflows/test-external.yml b/.github/workflows/test-external.yml index 6b75ba4c..fd75f3ca 100644 --- a/.github/workflows/test-external.yml +++ b/.github/workflows/test-external.yml @@ -28,8 +28,10 @@ jobs: - repo: "morpho-org/morpho-blue" dir: "morpho-blue" cmd: "--solver-threads 3" - branch: "" - profile: "test" + # morpho-blue removed its halmos tests in January 2026 (d788823); + # pin the last commit that still has test/halmos/HalmosTest.sol + branch: "e773d85cd24d5162c836ec01ebcfa1d84550b668" + profile: "" - repo: "a16z/cicada" dir: "cicada" cmd: "--contract LibUint1024Test --function testProve --loop 256 --solver-threads 3" From 5b0e54cd93e4874b8b1dfe917d6152137a762c6f Mon Sep 17 00:00:00 2001 From: Hugo DE SA PEREIRA PINTO Date: Tue, 22 Sep 2026 23:30:14 +0000 Subject: [PATCH 29/46] fix: symbolic SHA256/RIPEMD160/MODEXP precompiles (halmos#402) The precompile input size was passed to BitVecSorts in bytes instead of bits, so any symbolic call to SHA256, RIPEMD160 or MODEXP raised a z3 sort mismatch. - move the hashing and MODEXP models to a new precompiles module - evaluate concrete inputs exactly (hashlib, pow) - MODEXP: EIP-198 layout with zero-padded input, output on len(M) bytes, EIP-7823 input length cap; symbolic results are modeled with an uninterpreted function constrained by result < M, with exact handling of M == 0, E == 0, E == 1 and bases 0/1 - add tests/regression/test/Precompiles.t.sol --- CHANGELOG.md | 8 + src/dolmos/precompiles.py | 187 ++++++++++++++++++++++++ src/dolmos/sevm.py | 42 ++---- tests/expected/all.json | 119 +++++++++++++++ tests/regression/test/Precompiles.t.sol | 107 ++++++++++++++ 5 files changed, 432 insertions(+), 31 deletions(-) create mode 100644 src/dolmos/precompiles.py create mode 100644 tests/regression/test/Precompiles.t.sol diff --git a/CHANGELOG.md b/CHANGELOG.md index f6e37996..f4f1601f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ All notable changes to dolmos are documented here. The format follows ## [Unreleased] +### Fixed + +- SHA256, RIPEMD160 and MODEXP precompiles with symbolic input no longer crash + with a z3 sort mismatch (input sizes were given in bytes instead of bits). + Concrete inputs are now evaluated exactly; MODEXP follows EIP-198 (zero-padded + input, output on `len(M)` bytes) and EIP-7823 (lengths above 1024 bytes make + the call fail), and its symbolic model guarantees `result < M` (halmos#402) + ## [0.1.0] - 2026-09-22 First release of dolmos. diff --git a/src/dolmos/precompiles.py b/src/dolmos/precompiles.py new file mode 100644 index 00000000..26fdd9f3 --- /dev/null +++ b/src/dolmos/precompiles.py @@ -0,0 +1,187 @@ +# SPDX-License-Identifier: AGPL-3.0 + +""" +Models for the hashing and modular exponentiation precompiles. + +The precompile input is a ByteVec whose length is in *bytes*, while z3 sorts are +sized in *bits*. Mixing the two up produced sort mismatches for every symbolic +call to SHA256, RIPEMD160 and MODEXP (halmos issue #402). + +Concrete inputs are evaluated exactly. Symbolic inputs are modeled with +uninterpreted functions constrained by the properties that always hold. +""" + +import hashlib + +from z3 import ( + ULT, + BitVecRef, + Extract, + Function, + If, + Implies, + Or, + ZeroExt, + is_bv, +) + +from dolmos.bytevec import ByteVec +from dolmos.utils import BitVecSorts, bytes_to_bv_value, con + +# EIP-7823 (osaka): each length field of a MODEXP input is capped at 1024 bytes; +# larger inputs make the precompile fail +MODEXP_MAX_INPUT_SIZE = 1024 + +# ripemd160(b"") +EMPTY_RIPEMD160 = bytes.fromhex("9c1185a5c5e9fc54612808977ee8f548b2258d31") + + +def _as_bv(data: bytes | BitVecRef) -> BitVecRef: + """Convert non-empty unwrapped bytes to a bitvector.""" + return data if is_bv(data) else bytes_to_bv_value(data) + + +def _ripemd160(data: bytes) -> bytes | None: + if not data: + return EMPTY_RIPEMD160 + try: + return hashlib.new("ripemd160", data).digest() + except ValueError: + # not available in some OpenSSL 3 builds + return None + + +def sha256(arg: ByteVec) -> ByteVec: + data = arg.unwrap() + + if not is_bv(data): + return ByteVec(hashlib.sha256(data).digest()) + + size_bits = len(arg) * 8 + f_sha256 = Function( + f"f_sha256_{len(arg)}", BitVecSorts[size_bits], BitVecSorts[256] + ) + return ByteVec(f_sha256(data)) + + +def ripemd160(arg: ByteVec) -> ByteVec: + data = arg.unwrap() + + # the 20-byte digest is returned left-padded to 32 bytes + if not is_bv(data) and (digest := _ripemd160(data)) is not None: + return ByteVec(bytes(12) + digest) + + size_bits = len(arg) * 8 + f_ripemd160 = Function( + f"f_ripemd160_{len(arg)}", BitVecSorts[size_bits], BitVecSorts[160] + ) + return ByteVec(ZeroExt(96, f_ripemd160(_as_bv(data)))) + + +def modexp(ex, arg: ByteVec) -> ByteVec | None: + """ + Returns the output of MODEXP, or None if the precompile fails. + + Input layout (EIP-198): + + each length is a 32-byte word; missing input bytes are read as zeros. + The output is (B ** E) % M, encoded on len(M) bytes (0 if M == 0). + """ + + b_size = ex.int_of(arg.get_word(0), "symbolic MODEXP base length") + e_size = ex.int_of(arg.get_word(32), "symbolic MODEXP exponent length") + m_size = ex.int_of(arg.get_word(64), "symbolic MODEXP modulus length") + + if max(b_size, e_size, m_size) > MODEXP_MAX_INPUT_SIZE: + return None + + if m_size == 0: + return ByteVec() + + b_start = 96 + e_start = b_start + b_size + m_start = e_start + e_size + + b = arg.slice(b_start, e_start).unwrap() if b_size else b"" + e = arg.slice(e_start, m_start).unwrap() if e_size else b"" + m = arg.slice(m_start, m_start + m_size).unwrap() + + # fully concrete: compute the exact result + if not any(is_bv(x) for x in (b, e, m)): + m_int = int.from_bytes(m, "big") + if m_int == 0: + return ByteVec(bytes(m_size)) + b_int = int.from_bytes(b, "big") + e_int = int.from_bytes(e, "big") + return ByteVec(pow(b_int, e_int, m_int).to_bytes(m_size, "big")) + + m_bits = m_size * 8 + m_bv = _as_bv(m) + zero_m, one_m = con(0, m_bits), con(1, m_bits) + + # x ** 0 == 1, and 1 % 1 == 0 + one_mod_m = If(m_bv == one_m, zero_m, one_m) + + # b % m, computed on a common width. + # like the MOD opcode, the (nonlinear) remainder is abstracted with an + # uninterpreted function: z3 rewrites a guarded URem into its internal + # `bvurem_i` operator, which external solvers don't understand. + width = max(b_size, m_size) * 8 + + def mod_m(x: BitVecRef, x_bits: int) -> BitVecRef: + x_ext = ZeroExt(width - x_bits, x) if width > x_bits else x + m_ext = ZeroExt(width - m_bits, m_bv) if width > m_bits else m_bv + f_rem = Function( + f"f_evm_bvurem_{width}", + BitVecSorts[width], + BitVecSorts[width], + BitVecSorts[width], + ) + r = f_rem(x_ext, m_ext) + ex.path.append(Implies(m_ext != 0, ULT(r, m_ext))) + return r if width == m_bits else Extract(m_bits - 1, 0, r) + + b_bv = _as_bv(b) if b_size else None + b_mod_m = mod_m(b_bv, b_size * 8) if b_size else zero_m + + e_is_concrete = not is_bv(e) + e_int = int.from_bytes(e, "big") if e_is_concrete else None + + if e_is_concrete and e_int == 0: + result = one_mod_m + + elif e_is_concrete and e_int == 1: + result = b_mod_m + + elif b_size == 0: + # 0 ** e == 0 for e > 0 + result = zero_m if e_is_concrete else If(_as_bv(e) == 0, one_mod_m, zero_m) + + else: + e_bv = con(e_int, e_size * 8) if e_is_concrete else _as_bv(e) + f_modexp = Function( + f"f_modexp_{b_size}_{e_size}_{m_size}", + BitVecSorts[b_size * 8], + BitVecSorts[e_size * 8], + BitVecSorts[m_bits], + BitVecSorts[m_bits], + ) + f_val = f_modexp(b_bv, e_bv, m_bv) + + # properties of the abstraction: + # - the result is reduced modulo m + # - base 0 or 1, and exponent 1, are computed exactly + ex.path.append(Implies(m_bv != zero_m, ULT(f_val, m_bv))) + + result = If( + e_bv == 0, + one_mod_m, + If( + Or(b_bv == 0, b_bv == 1, e_bv == 1), + b_mod_m, + f_val, + ), + ) + + result = If(m_bv == zero_m, zero_m, result) + return ByteVec(result) diff --git a/src/dolmos/sevm.py b/src/dolmos/sevm.py index fddebdfe..98aac939 100644 --- a/src/dolmos/sevm.py +++ b/src/dolmos/sevm.py @@ -54,6 +54,7 @@ ) from z3.z3util import is_expr_var +from dolmos import precompiles from dolmos.bitvec import ONE, ZERO, is_power_of_two from dolmos.bitvec import DolmosBitVec as BV from dolmos.bitvec import DolmosBool as Bool @@ -2588,46 +2589,25 @@ def call_unknown() -> None: elif to == SHA256_PRECOMPILE: exit_code = ONE - f_sha256 = Function( - f"f_sha256_{arg_size}", BitVecSorts[arg_size], BitVecSort256 - ) - - unwrapped = arg.unwrap() - wrapped = ( - unwrapped if is_bv(unwrapped) else bytes_to_bv_value(unwrapped) - ) - ret = ByteVec(f_sha256(wrapped)) + ret = precompiles.sha256(arg) elif to == RIPEMD160_PRECOMPILE: exit_code = ONE - f_ripemd160 = Function( - f"f_ripemd160_{arg_size}", BitVecSorts[arg_size], BitVecSort160 - ) - - unwrapped = arg.unwrap() - wrapped = ( - unwrapped if is_bv(unwrapped) else bytes_to_bv_value(unwrapped) - ) - ret = ByteVec(uint256(f_ripemd160(wrapped))) + ret = precompiles.ripemd160(arg) elif to == IDENTITY_PRECOMPILE: exit_code = ONE ret = arg elif to == MODEXP_PRECOMPILE: - exit_code = ONE - modulus_size = ex.int_of(arg.get_word(64)) - f_modexp = Function( - f"f_modexp_{arg_size}_{modulus_size}", - BitVecSorts[arg_size], - BitVecSorts[modulus_size], - ) - - unwrapped = arg.unwrap() - wrapped = ( - unwrapped if is_bv(unwrapped) else bytes_to_bv_value(unwrapped) - ) - ret = ByteVec(f_modexp(wrapped)) + modexp_ret = precompiles.modexp(ex, arg) + if modexp_ret is None: + # invalid input (EIP-7823): the call fails with empty returndata + exit_code = ZERO + ret = ByteVec() + else: + exit_code = ONE + ret = modexp_ret elif to == ECADD_PRECOMPILE: exit_code = ONE diff --git a/tests/expected/all.json b/tests/expected/all.json index ed92c1e4..0b57ff8d 100644 --- a/tests/expected/all.json +++ b/tests/expected/all.json @@ -3712,6 +3712,125 @@ "num_bounded_loops": null } ], + "test/Precompiles.t.sol:PrecompilesTest": [ + { + "name": "check_modexp_concrete()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_modexp_empty_modulus(uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_modexp_exp_zero(uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_modexp_mod_zero(uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_modexp_oversized_input()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_modexp_short_input()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_modexp_symbolic(uint256,uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_modexp_symbolic_fail(uint256,uint256,uint256)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_ripemd160_concrete()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_ripemd160_symbolic(uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_sha256_concrete()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_sha256_symbolic(uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_sha256_symbolic_fail(uint256,uint256)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], "test/Proxy.t.sol:ProxyTest": [ { "name": "check_foo(uint256,uint256,address)", diff --git a/tests/regression/test/Precompiles.t.sol b/tests/regression/test/Precompiles.t.sol new file mode 100644 index 00000000..86cab2dd --- /dev/null +++ b/tests/regression/test/Precompiles.t.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import "forge-std/Test.sol"; + +/// @dev regression tests for halmos issue #402: symbolic inputs to the SHA256, +/// RIPEMD160 and MODEXP precompiles used to crash with a z3 sort mismatch +contract PrecompilesTest is Test { + function modexp(uint256 b, uint256 e, uint256 m) internal view returns (bool ok, uint256 r) { + bytes memory input = abi.encodePacked(uint256(32), uint256(32), uint256(32), b, e, m); + bytes memory out; + (ok, out) = address(5).staticcall(input); + if (ok && out.length == 32) r = abi.decode(out, (uint256)); + } + + // + // sha256 + // + + function check_sha256_concrete() public pure { + assertEq(sha256(""), 0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855); + assertEq(sha256("abc"), 0xba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad); + } + + function check_sha256_symbolic(uint256 x, uint256 y) public pure { + vm.assume(x == y); + assertEq(sha256(abi.encode(x)), sha256(abi.encode(y))); + } + + function check_sha256_symbolic_fail(uint256 x, uint256 y) public pure { + // uninterpreted: dolmos can't rule out a collision + assertTrue(sha256(abi.encode(x)) != sha256(abi.encode(y))); + } + + // + // ripemd160 + // + + function check_ripemd160_concrete() public pure { + assertEq(ripemd160(""), bytes20(hex"9c1185a5c5e9fc54612808977ee8f548b2258d31")); + assertEq(ripemd160("abc"), bytes20(hex"8eb208f7e05d987a9b044a8e98c6b087f15a0bfc")); + } + + function check_ripemd160_symbolic(uint256 x, uint256 y) public pure { + vm.assume(x == y); + assertEq(ripemd160(abi.encode(x)), ripemd160(abi.encode(y))); + } + + // + // modexp + // + + function check_modexp_concrete() public view { + (bool ok, uint256 r) = modexp(3, 5, 7); + assertTrue(ok); + assertEq(r, 5); // 243 % 7 + } + + function check_modexp_mod_zero(uint256 b, uint256 e) public view { + (bool ok, uint256 r) = modexp(b, e, 0); + assertTrue(ok); + assertEq(r, 0); + } + + function check_modexp_exp_zero(uint256 b, uint256 m) public view { + vm.assume(m > 1); + (bool ok, uint256 r) = modexp(b, 0, m); + assertTrue(ok); + assertEq(r, 1); + } + + function check_modexp_symbolic(uint256 b, uint256 e, uint256 m) public view { + vm.assume(m != 0); + (bool ok, uint256 r) = modexp(b, e, m); + assertTrue(ok); + assertLt(r, m); + } + + function check_modexp_symbolic_fail(uint256 b, uint256 e, uint256 m) public view { + vm.assume(m > 2); + (, uint256 r) = modexp(b, e, m); + assertEq(r, 1); // counterexample expected + } + + function check_modexp_short_input() public view { + // missing bytes are zeros: base = 2, exponent = 3, modulus = 0x05 << 8 + (bool ok, bytes memory out) = address(5).staticcall( + abi.encodePacked(uint256(1), uint256(1), uint256(2), uint8(2), uint8(3), uint8(5)) + ); + assertTrue(ok); + assertEq(out.length, 2); + assertEq(uint16(bytes2(out)), 8); + } + + function check_modexp_empty_modulus(uint256 b, uint256 e) public view { + (bool ok, bytes memory out) = + address(5).staticcall(abi.encodePacked(uint256(32), uint256(32), uint256(0), b, e)); + assertTrue(ok); + assertEq(out.length, 0); + } + + function check_modexp_oversized_input() public view { + // EIP-7823: length fields above 1024 bytes make the precompile fail + (bool ok,) = address(5).staticcall(abi.encodePacked(uint256(1025), uint256(1), uint256(1))); + assertFalse(ok); + } +} From d5e1851481b5f6b3ea81d6b0583b88c91e42dcd4 Mon Sep 17 00:00:00 2001 From: Hugo DE SA PEREIRA PINTO Date: Wed, 23 Sep 2026 07:26:16 +0000 Subject: [PATCH 30/46] feat: vm.expectRevert and vm.expectEmit cheatcodes (halmos#409) Both cheatcodes used to fail with "Unsupported cheat code". - new expectations module: per-frame expectations stored in CallContext, decoding of the 26 overloads, symbolic matching of revert data and logs - an expectation applies to the next call or creation of the frame (cheatcode and console calls are ignored); a call that reverts as expected is reported as successful with dummy return data, like foundry - expectRevert: raw data or Error(string) payload match, partial match, reverter address, count (0 = must not revert) - expectEmit: ordered match anywhere in the call tree, topic/data checks, emitter address, anonymous events, count - a frame that returns with a pending expectation fails the test - failing expectations fork a FailCheatcode path, so symbolic inputs yield counterexamples - add tests/regression/test/Expect.t.sol (35 tests); use expectCall as the example of an unsupported cheatcode in SvmCheatCode.t.sol --- CHANGELOG.md | 9 + docs/getting-started.md | 9 + src/dolmos/cheatcodes.py | 16 + src/dolmos/expectations.py | 461 +++++++++++++++++++++++ src/dolmos/sevm.py | 186 ++++++++- src/dolmos/utils.py | 26 -- tests/expected/all.json | 317 ++++++++++++++++ tests/regression/test/Expect.t.sol | 315 ++++++++++++++++ tests/regression/test/SvmCheatCode.t.sol | 2 +- 9 files changed, 1308 insertions(+), 33 deletions(-) create mode 100644 src/dolmos/expectations.py create mode 100644 tests/regression/test/Expect.t.sol diff --git a/CHANGELOG.md b/CHANGELOG.md index f4f1601f..46c690a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ All notable changes to dolmos are documented here. The format follows ## [Unreleased] +### Added + +- `vm.expectRevert` (all overloads, including the reverter address and `count` + variants), `vm.expectPartialRevert`, `vm.expectEmit` and + `vm.expectEmitAnonymous` (including the emitter address and `count` variants), + with foundry's semantics; symbolic revert data and event fields are matched + symbolically and a counterexample is reported when an expectation can be + violated (halmos#409) + ### Fixed - SHA256, RIPEMD160 and MODEXP precompiles with symbolic input no longer crash diff --git a/docs/getting-started.md b/docs/getting-started.md index 497c5511..4b38fc37 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -196,6 +196,15 @@ token.transfer(receiver, amount); } ``` +- Foundry's `vm.expectRevert()` (all variants, including `expectPartialRevert`, the reverter address and the `count` overloads) and `vm.expectEmit()` (including `expectEmitAnonymous`, the emitter address and the `count` overloads) are supported, with the same semantics as in foundry. With symbolic inputs, dolmos reports a counterexample whenever the expectation can be violated: + ```solidity + vm.assume(amount > balanceOfSender); + vm.expectRevert(abi.encodeWithSelector(Token.InsufficientBalance.selector, sender)); + vm.prank(sender); + token.transfer(receiver, amount); + ``` + Note that like foundry, logs emitted by reverted subcalls are taken into account by `expectEmit`. + ### 2.4 Check output states After calling the target contracts, you can write assertions against the output state of the contracts. diff --git a/src/dolmos/cheatcodes.py b/src/dolmos/cheatcodes.py index f4ed7bcc..b2e93d71 100644 --- a/src/dolmos/cheatcodes.py +++ b/src/dolmos/cheatcodes.py @@ -44,6 +44,12 @@ NotConcreteError, OutOfGasError, ) +from dolmos.expectations import ( + EMIT_SIGNATURES, + REVERT_SIGNATURES, + parse_expect_emit, + parse_expect_revert, +) from dolmos.logs import debug from dolmos.mapper import BuildOut from dolmos.utils import ( @@ -1619,6 +1625,16 @@ def handle(sevm, ex, arg: ByteVec, stack) -> ByteVec | None: elif funsig == hevm_cheat_code.env_exists_sig: return env_exists(arg) + # vm.expectRevert(...) / vm.expectPartialRevert(...) + elif funsig in REVERT_SIGNATURES: + ex.context.expectations.expect_revert(parse_expect_revert(funsig, arg)) + return ret + + # vm.expectEmit(...) / vm.expectEmitAnonymous(...) + elif funsig in EMIT_SIGNATURES: + ex.context.expectations.expect_emit(parse_expect_emit(funsig, arg)) + return ret + elif funsig in dict_of_unsupported_cheatcodes: msg = f"Unsupported cheat code: {dict_of_unsupported_cheatcodes[funsig]}" raise DolmosException(msg) diff --git a/src/dolmos/expectations.py b/src/dolmos/expectations.py new file mode 100644 index 00000000..c4a719c7 --- /dev/null +++ b/src/dolmos/expectations.py @@ -0,0 +1,461 @@ +# SPDX-License-Identifier: AGPL-3.0 + +""" +Support for the `vm.expectRevert*` / `vm.expectPartialRevert*` and +`vm.expectEmit*` / `vm.expectEmitAnonymous*` cheatcodes (halmos issue #409). + +The semantics follow foundry: + +- an expectation is set by a cheatcode call and applies to the *next call* (or + contract creation) made by the same frame; calls to cheatcode addresses and to + the console are ignored. +- expectRevert: the next call must revert, with a matching reason and reverter if + given. When the expectation is met the call is reported as successful to the + caller, with dummy return data (like foundry). `count = n` requires the next n + calls to revert, `count = 0` requires the next call not to revert. +- expectEmit: the next event emitted by the frame itself is recorded as the + expected event. The next call must then emit events matching the expected ones + (in order, anywhere in its call tree). With a `count`, the expected event must + be emitted exactly `count` times. +- an expectation that is still pending when the frame returns successfully makes + the test fail. + +Because data may be symbolic, matching produces an SMT condition: the caller +forks a failing path when the condition can be false. +""" + +from collections.abc import Iterator +from dataclasses import dataclass, field + +from z3 import ( + And, + BitVecRef, + BoolRef, + BoolVal, + Concat, + Extract, + If, + Or, + Sum, + is_bv, + simplify, +) + +from dolmos.bitvec import DolmosBitVec as BV +from dolmos.bytevec import ByteVec +from dolmos.exceptions import DolmosException +from dolmos.utils import ( + bytes_to_bv_value, + con, + extract_bytes_argument, + int_of, + uint160, +) + +# foundry reports a successful call with 8192 zero bytes when a revert was expected +DUMMY_CALL_OUTPUT = ByteVec(bytes(8192)) + +# bytes4(keccak256("Error(string)")) +ERROR_STRING_SELECTOR = bytes.fromhex("08c379a0") + +UnwrappedBytes = bytes | BitVecRef + + +# +# expectation records +# + + +@dataclass +class ExpectedRevert: + reason: UnwrappedBytes | None = None # None: any revert data + partial: bool = False # only compare the first 4 bytes + reverter: BitVecRef | None = None # None: any reverter + count: int = 1 # 0: the next call must not revert + + def __str__(self) -> str: + parts = [] + if self.reason is not None: + parts.append(f"reason={_hex(self.reason)}") + if self.reverter is not None: + parts.append(f"reverter={self.reverter}") + if self.count != 1: + parts.append(f"count={self.count}") + name = "expectPartialRevert" if self.partial else "expectRevert" + return f"{name}({', '.join(parts)})" + + +@dataclass +class ExpectedEmit: + # checks for topic0 (anonymous events only), topic1, topic2, topic3, data + checks: tuple[bool, bool, bool, bool, bool] + anonymous: bool = False + emitter: BitVecRef | None = None + count: int | None = None # None: plain (ordered) expectation + log: object | None = None # the expected EventLog, once emitted + + def __str__(self) -> str: + name = "expectEmitAnonymous" if self.anonymous else "expectEmit" + emitter = f", emitter={self.emitter}" if self.emitter is not None else "" + count = f", count={self.count}" if self.count is not None else "" + return f"{name}(checks={self.checks}{emitter}{count})" + + +@dataclass +class Expectations: + """ + Pending expectations of a call frame. Stored in its CallContext. + """ + + revert: ExpectedRevert | None = None + emits: list[ExpectedEmit] = field(default_factory=list) + + def __bool__(self) -> bool: + return self.revert is not None or bool(self.emits) + + def expect_revert(self, expected: ExpectedRevert) -> None: + if self.revert is not None: + raise DolmosException( + "expectRevert: an expected revert is already pending; " + "call another function before expecting a second revert" + ) + self.revert = expected + + def expect_emit(self, expected: ExpectedEmit) -> None: + self.emits.append(expected) + + def capture_log(self, log) -> None: + """Records `log` as the expected event of the first unfilled expectEmit.""" + for spec in self.emits: + if spec.log is None: + spec.log = log + return + + def take(self) -> "Expectations | None": + """ + Returns the expectations that apply to the next call, and clears them. + """ + if not self: + return None + + taken = Expectations(revert=self.revert, emits=self.emits) + self.revert = None + self.emits = [] + return taken + + def describe(self) -> str: + items = ([str(self.revert)] if self.revert else []) + [ + str(e) for e in self.emits + ] + return ", ".join(items) + + +# +# cheatcode argument decoding +# + + +def _word(arg: ByteVec, idx: int): + return arg.get_word(4 + 32 * idx) + + +def _bool(arg: ByteVec, idx: int, name: str) -> bool: + return int_of(_word(arg, idx), f"symbolic {name} argument") != 0 + + +def _address(arg: ByteVec, idx: int) -> BitVecRef: + return uint160(_word(arg, idx)).as_z3() + + +def _uint64(arg: ByteVec, idx: int, name: str) -> int: + return int_of(_word(arg, idx), f"symbolic {name} count") & (2**64 - 1) + + +def _bytes4(arg: ByteVec, idx: int) -> UnwrappedBytes: + start = 4 + 32 * idx + return arg.slice(start, start + 4).unwrap() + + +# expectRevert variants: signature -> argument layout +REVERT_SIGNATURES: dict[int, tuple[str, tuple[str, ...]]] = { + 0xF4844814: ("expectRevert()", ()), + 0xC31EB0E0: ("expectRevert(bytes4)", ("bytes4",)), + 0xF28DCEB3: ("expectRevert(bytes)", ("bytes",)), + 0xD814F38A: ("expectRevert(address)", ("address",)), + 0x260BC5DE: ("expectRevert(bytes4,address)", ("bytes4", "address")), + 0x61EBCF12: ("expectRevert(bytes,address)", ("bytes", "address")), + 0x4EE38244: ("expectRevert(uint64)", ("count",)), + 0xE45CA72D: ("expectRevert(bytes4,uint64)", ("bytes4", "count")), + 0x4994C273: ("expectRevert(bytes,uint64)", ("bytes", "count")), + 0x1FF5F952: ("expectRevert(address,uint64)", ("address", "count")), + 0xB0762D73: ( + "expectRevert(bytes4,address,uint64)", + ("bytes4", "address", "count"), + ), + 0xD345FB1F: ( + "expectRevert(bytes,address,uint64)", + ("bytes", "address", "count"), + ), + 0x11FB5B9C: ("expectPartialRevert(bytes4)", ("bytes4",)), + 0x51AA008A: ("expectPartialRevert(bytes4,address)", ("bytes4", "address")), +} + +# expectEmit variants: signature -> (anonymous, argument layout) +EMIT_SIGNATURES: dict[int, tuple[str, bool, tuple[str, ...]]] = { + 0x440ED10D: ("expectEmit()", False, ()), + 0x86B9620D: ("expectEmit(address)", False, ("address",)), + 0x491CC7C2: ("expectEmit(bool,bool,bool,bool)", False, ("bool",) * 4), + 0x81BAD6F3: ( + "expectEmit(bool,bool,bool,bool,address)", + False, + ("bool",) * 4 + ("address",), + ), + 0x4C74A335: ("expectEmit(uint64)", False, ("count",)), + 0xB43AECE3: ("expectEmit(address,uint64)", False, ("address", "count")), + 0x5E1D1C33: ( + "expectEmit(bool,bool,bool,bool,uint64)", + False, + ("bool",) * 4 + ("count",), + ), + 0xC339D02C: ( + "expectEmit(bool,bool,bool,bool,address,uint64)", + False, + ("bool",) * 4 + ("address", "count"), + ), + 0x2E5F270C: ("expectEmitAnonymous()", True, ()), + 0x6FC68705: ("expectEmitAnonymous(address)", True, ("address",)), + 0xC948DB5E: ( + "expectEmitAnonymous(bool,bool,bool,bool,bool)", + True, + ("bool",) * 5, + ), + 0x71C95899: ( + "expectEmitAnonymous(bool,bool,bool,bool,bool,address)", + True, + ("bool",) * 5 + ("address",), + ), +} + + +def parse_expect_revert(funsig: int, arg: ByteVec) -> ExpectedRevert: + name, layout = REVERT_SIGNATURES[funsig] + expected = ExpectedRevert(partial=name.startswith("expectPartialRevert")) + + for idx, kind in enumerate(layout): + match kind: + case "bytes4": + expected.reason = _bytes4(arg, idx) + case "bytes": + expected.reason = extract_bytes_argument(arg, idx) + case "address": + expected.reverter = _address(arg, idx) + case "count": + expected.count = _uint64(arg, idx, name) + + return expected + + +def parse_expect_emit(funsig: int, arg: ByteVec) -> ExpectedEmit: + name, anonymous, layout = EMIT_SIGNATURES[funsig] + + bools = [_bool(arg, i, name) for i, k in enumerate(layout) if k == "bool"] + if not bools: + bools = [True] * (5 if anonymous else 4) + + # normalize to (topic0, topic1, topic2, topic3, data); + # topic0 (the event selector) is always checked for non-anonymous events + checks = tuple(bools) if anonymous else (True, *bools) + + expected = ExpectedEmit(checks=checks, anonymous=anonymous) + for idx, kind in enumerate(layout): + match kind: + case "address": + expected.emitter = _address(arg, idx) + case "count": + expected.count = _uint64(arg, idx, name) + + return expected + + +# +# symbolic matching +# + + +def _unwrap(x) -> UnwrappedBytes: + if isinstance(x, ByteVec): + return x.unwrap() + return x + + +def _len(x: UnwrappedBytes) -> int: + return x.size() // 8 if is_bv(x) else len(x) + + +def _slice(x: UnwrappedBytes, start: int, stop: int) -> UnwrappedBytes: + """Byte slice [start:stop), zero-padded if out of bounds.""" + n = _len(x) + if stop <= start: + return b"" + if not is_bv(x): + return (x + bytes(max(0, stop - n)))[start:stop] + if start >= n: + return bytes(stop - start) + hi = (n - start) * 8 - 1 + lo = max(0, (n - stop) * 8) + part = Extract(hi, lo, x) + pad = stop - min(stop, n) + return simplify(Concat(part, con(0, pad * 8))) if pad else simplify(part) + + +def _to_bv(x: UnwrappedBytes) -> BitVecRef: + return x if is_bv(x) else bytes_to_bv_value(x) + + +def eq_bytes(a, b) -> BoolRef: + a, b = _unwrap(a), _unwrap(b) + if _len(a) != _len(b): + return BoolVal(False) + if _len(a) == 0: + return BoolVal(True) + if not is_bv(a) and not is_bv(b): + return BoolVal(a == b) + return _to_bv(a) == _to_bv(b) + + +def as_z3_word(x): + if isinstance(x, BV): + return x.as_z3() + if isinstance(x, int): + return con(x) + return x + + +def revert_reason_matches(expected: ExpectedRevert, data: ByteVec) -> BoolRef: + """ + Whether revert `data` matches the expected reason: + - the raw revert data equals the reason, or + - the data is `Error(string)` and its payload equals the reason, or + - for partial matches, the first 4 bytes are equal. + """ + if expected.reason is None: + return BoolVal(True) + + reason = expected.reason + raw = _unwrap(data) + reason_len, data_len = _len(reason), _len(raw) + + conds = [eq_bytes(raw, reason)] + + if expected.partial and reason_len >= 4 and data_len >= 4: + conds.append(eq_bytes(_slice(raw, 0, 4), _slice(reason, 0, 4))) + + # Error(string) unwrapping: selector | offset (0x20) | length | payload + payload_end = 68 + reason_len + if data_len >= payload_end: + conds.append( + And( + eq_bytes(_slice(raw, 0, 4), ERROR_STRING_SELECTOR), + eq_bytes(_slice(raw, 4, 36), (32).to_bytes(32, "big")), + eq_bytes(_slice(raw, 36, 68), reason_len.to_bytes(32, "big")), + eq_bytes(_slice(raw, 68, payload_end), reason), + ) + ) + + return simplify(Or(*conds)) + + +def find_reverter(subcall) -> BitVecRef: + """ + The address that originally reverted: follow the chain of reverted subcalls + that bubbled up the same revert data. + """ + ctx = subcall + while True: + child = ctx.last_subcall() + if ( + child is None + or child.output.error is None + or child.output.data is None + or child.output.data != ctx.output.data + ): + return ctx.message.target + ctx = child + + +def iter_logs(ctx) -> Iterator: + """All the logs emitted in a call tree, in execution order.""" + for item in ctx.trace: + if hasattr(item, "topics"): + yield item + elif hasattr(item, "trace"): + yield from iter_logs(item) + + +def log_matches(spec: ExpectedEmit, log) -> BoolRef: + expected = spec.log + conds = [] + + if spec.emitter is not None: + conds.append(as_z3_word(log.address) == as_z3_word(spec.emitter)) + + if len(log.topics) != len(expected.topics): + return BoolVal(False) + + for i, (actual, wanted) in enumerate(zip(log.topics, expected.topics, strict=True)): + if spec.checks[i]: + conds.append(as_z3_word(actual) == as_z3_word(wanted)) + + if spec.checks[4]: + conds.append(eq_bytes(log.data or b"", expected.data or b"")) + + return simplify(And(*conds)) if conds else BoolVal(True) + + +def emits_match(specs: list[ExpectedEmit], subcall) -> tuple[BoolRef, str | None]: + """ + Returns (condition, error). A non-None error means the expectation can't be met. + """ + unfilled = [s for s in specs if s.log is None] + if unfilled: + return BoolVal(False), ( + f"{unfilled[0]}: no event was emitted between the cheatcode and the next call" + ) + + logs = list(iter_logs(subcall)) + conds = [] + + # counted expectations are checked independently + for spec in (s for s in specs if s.count is not None): + hits = [If(log_matches(spec, log), con(1), con(0)) for log in logs] + total = simplify(Sum(hits)) if hits else con(0) + conds.append(total == con(spec.count)) + + # plain expectations must appear as an ordered subsequence of the logs + ordered = [s for s in specs if s.count is None] + memo: dict[tuple[int, int], BoolRef] = {} + + def matches_from(i: int, j: int) -> BoolRef: + if i == len(ordered): + return BoolVal(True) + if len(ordered) - i > len(logs) - j: + return BoolVal(False) + if (i, j) not in memo: + memo[i, j] = simplify( + Or( + And(log_matches(ordered[i], logs[j]), matches_from(i + 1, j + 1)), + matches_from(i, j + 1), + ) + ) + return memo[i, j] + + if ordered: + conds.append(matches_from(0, 0)) + + return simplify(And(*conds)) if conds else BoolVal(True), None + + +def _hex(x: UnwrappedBytes) -> str: + if is_bv(x): + return str(x) + return "0x" + x.hex() diff --git a/src/dolmos/sevm.py b/src/dolmos/sevm.py index 98aac939..3f97aad1 100644 --- a/src/dolmos/sevm.py +++ b/src/dolmos/sevm.py @@ -5,7 +5,7 @@ from collections import Counter, defaultdict from collections.abc import Callable, Iterator from copy import deepcopy -from dataclasses import dataclass, field +from dataclasses import dataclass, field, replace from datetime import timedelta from functools import reduce from timeit import default_timer as timer @@ -179,6 +179,14 @@ StackUnderflowError, WriteInStaticContext, ) +from dolmos.expectations import ( + DUMMY_CALL_OUTPUT, + Expectations, + as_z3_word, + emits_match, + find_reverter, + revert_reason_matches, +) from dolmos.logs import ( INTERNAL_ERROR, debug, @@ -582,6 +590,11 @@ class CallContext: depth: int = 1 trace: list[TraceElement] = field(default_factory=list) prank: Prank = field(default_factory=Prank) + # pending vm.expectRevert / vm.expectEmit expectations of this frame + expectations: Expectations = field(default_factory=Expectations) + # return data seen by the caller, if different from output.data + # (a call that reverted as expected is reported as successful) + returndata_override: ByteVec | None = None def subcalls(self) -> Iterator["CallContext"]: return iter(t for t in self.trace if isinstance(t, CallContext)) @@ -1714,6 +1727,9 @@ def returndata(self) -> ByteVec | None: if not last_subcall: return EMPTY_BYTES + if last_subcall.returndata_override is not None: + return last_subcall.returndata_override + output = last_subcall.output if last_subcall.message.is_create() and not output.error: return EMPTY_BYTES @@ -2380,8 +2396,106 @@ def resolve_address_alias( ex.alias[target] = addr return addr + def resolve_expectations( + self, + ex: Exec, + stack: Worklist, + expected: Expectations, + reverted: bool, + ret_loc: int = 0, + ret_size: int = 0, + ) -> Exec | None: + """ + Checks the vm.expectRevert / vm.expectEmit expectations against the call + that just returned (the last subcall of `ex`), before `ex` advances. + + Forks a failing path whenever an expectation may not be met. Returns the + state to continue with, or None if the expectations can't be met. + """ + subcall = ex.context.last_subcall() + checks: list[tuple[BoolRef, str]] = [] + + exp_revert = expected.revert + reverted_as_expected = False + + if exp_revert is not None: + if exp_revert.count == 0: + if reverted: + checks.append((BoolVal(False), f"{exp_revert}: the call reverted")) + elif not reverted: + checks.append( + (BoolVal(False), f"{exp_revert}: next call did not revert") + ) + else: + data = ( + subcall.output.data + if subcall.output.data is not None + else ByteVec() + ) + cond = revert_reason_matches(exp_revert, data) + if exp_revert.reverter is not None: + reverter = as_z3_word(find_reverter(subcall)) + cond = And(cond, reverter == as_z3_word(exp_revert.reverter)) + checks.append( + (cond, f"{exp_revert}: the revert data or reverter differs") + ) + reverted_as_expected = True + + if expected.emits: + cond, err = emits_match(expected.emits, subcall) + msg = err or ( + f"{', '.join(map(str, expected.emits))}: " + "the expected events were not emitted by the next call" + ) + checks.append((cond, msg)) + + for cond, msg in checks: + cond = simplify(cond) + if is_true(cond): + continue + + if is_false(cond) or ex.check(cond) == unsat: + ex.halt(data=ByteVec(), error=FailCheatcode(msg)) + stack.push(ex) + return None + + not_cond = simplify(Not(cond)) + if ex.check(not_cond) != unsat: + fail_ex = self.create_branch(ex, not_cond, ex.pc) + fail_ex.halt(data=ByteVec(), error=FailCheatcode(msg)) + stack.push(fail_ex) + + ex.path.append(cond, branching=True) + + if reverted_as_expected: + # like foundry, report the call as successful: status 1 (address(1) + # for a creation) and dummy return data. The state stays reverted. + ex.st.pop() + ex.st.push(ONE) + if subcall.message.is_create(): + subcall.returndata_override = EMPTY_BYTES + else: + subcall.returndata_override = DUMMY_CALL_OUTPUT + copy_returndata_to_memory(DUMMY_CALL_OUTPUT, ret_loc, ret_size, ex) + + # expectRevert with a count applies to the following calls as well + if exp_revert.count > 1: + ex.context.expectations.revert = replace( + exp_revert, count=exp_revert.count - 1 + ) + + return ex + def handle_insufficient_fund_case( - self, caller: Address, value: BV, message: Message, ex: Exec, stack: Worklist + self, + caller: Address, + value: BV, + message: Message, + ex: Exec, + stack: Worklist, + expected: Expectations | None = None, + ret_loc: int = 0, + ret_size: int = 0, ): if value == ZERO: return @@ -2401,6 +2515,12 @@ def handle_insufficient_fund_case( ) ) fail_ex.st.push(ZERO) + if expected is not None: + fail_ex = self.resolve_expectations( + fail_ex, stack, expected, True, ret_loc, ret_size + ) + if fail_ex is None: + return fail_ex.advance() stack.push(fail_ex) @@ -2464,6 +2584,9 @@ def call( pranked_caller, pranked_origin = ex.resolve_prank(to) arg = ex.st.mslice(arg_loc, arg_size) + # vm.expectRevert / vm.expectEmit apply to the next non-cheatcode call + expected = None if to in CHEATCODE_ADDRESSES else ex.context.expectations.take() + resolved_to = to_alias if to_alias is not None else to message = Message( target=resolved_to if op in [OP_CALL, OP_STATICCALL] else ex.this(), @@ -2475,7 +2598,9 @@ def call( call_scheme=op, ) - self.handle_insufficient_fund_case(pranked_caller, fund, message, ex, stack) + self.handle_insufficient_fund_case( + pranked_caller, fund, message, ex, stack, expected, ret_loc, ret_size + ) def send_callvalue(condition: BoolRef | None = None) -> None: # no balance update for CALLCODE which transfers to itself @@ -2532,6 +2657,13 @@ def callback(new_ex: Exec, stack): new_ex.transient_storage = deepcopy(orig_transient_storage) new_ex.balance = orig_balance + if expected is not None: + new_ex = self.resolve_expectations( + new_ex, stack, expected, not subcall_success, ret_loc, ret_size + ) + if new_ex is None: + return + # add to worklist even if it reverted during the external call new_ex.advance() stack.push(new_ex) @@ -2724,6 +2856,14 @@ def call_unknown() -> None: ) ) + if expected is not None: + reverted = exit_code.is_concrete and exit_code.value == 0 + new_ex = self.resolve_expectations( + new_ex, stack, expected, reverted, ret_loc, ret_size + ) + if new_ex is None: + continue + new_ex.advance() stack.push(new_ex) @@ -2803,12 +2943,16 @@ def create( call_scheme=op, ) - self.handle_insufficient_fund_case(pranked_caller, value, message, ex, stack) + # vm.expectRevert / vm.expectEmit also apply to contract creations + expected = ex.context.expectations.take() + + self.handle_insufficient_fund_case( + pranked_caller, value, message, ex, stack, expected + ) if new_addr in ex.code: # address conflicts don't revert, they push 0 on the stack and continue ex.st.push(ZERO) - ex.advance() # add a virtual subcontext to the trace for debugging purposes subcall = CallContext(message=message, depth=ex.context.depth + 1) @@ -2816,6 +2960,12 @@ def create( subcall.output.error = AddressCollision() ex.context.trace.append(subcall) + if expected is not None: + ex = self.resolve_expectations(ex, stack, expected, True) + if ex is None: + return + + ex.advance() stack.push(ex) return @@ -2885,6 +3035,13 @@ def callback(new_ex: Exec, stack): new_ex.transient_storage = deepcopy(orig_transient_storage) new_ex.balance = orig_balance + if expected is not None: + new_ex = self.resolve_expectations( + new_ex, stack, expected, subcall.output.error is not None + ) + if new_ex is None: + return + # add to worklist new_ex.advance() stack.push(new_ex) @@ -3138,6 +3295,21 @@ def run(self, ex0: Exec) -> Iterator[Exec]: stack: Worklist = Worklist() def finalize(ex: Exec): + # a frame that returns successfully must not leave expectations pending + output = ex.context.output + if ( + output.error is None + and output.data is not None + and ex.context.expectations + ): + pending = ex.context.expectations.describe() + output.error = FailCheatcode( + f"{pending}: the expected call never happened" + ) + stack.completed_paths += 1 + yield ex # early exit, like other cheatcode failures + return + # if it's at the top-level, there is no callback; yield the current execution state if ex.callback is None: stack.completed_paths += 1 @@ -3493,7 +3665,9 @@ def finalize(ex: Exec): size: int = ex.int_of(state.pop(), "symbolic LOG data size") topics = list(state.pop() for _ in range(num_topics)) data = state.mslice(loc, size) - ex.emit_log(EventLog(ex.this(), topics, data)) + log = EventLog(ex.this(), topics, data) + ex.emit_log(log) + ex.context.expectations.capture_log(log) elif opcode in CALL_OPCODES: to = uint160(state.peek(2)) diff --git a/src/dolmos/utils.py b/src/dolmos/utils.py index b8926f91..9ea39893 100644 --- a/src/dolmos/utils.py +++ b/src/dolmos/utils.py @@ -1421,14 +1421,6 @@ def wrapper(*args, **kwargs): 0xE13A1834: "expectCallMinGas(address,uint256,uint64,bytes,uint64)", 0x73CDCE36: "expectCreate(bytes,address)", 0xEA54A472: "expectCreate2(bytes,address)", - 0xB43AECE3: "expectEmit(address,uint64)", - 0xC339D02C: "expectEmit(bool,bool,bool,bool,address,uint64)", - 0x5E1D1C33: "expectEmit(bool,bool,bool,bool,uint64)", - 0x4C74A335: "expectEmit(uint64)", - 0x2E5F270C: "expectEmitAnonymous()", - 0x6FC68705: "expectEmitAnonymous(address)", - 0xC948DB5E: "expectEmitAnonymous(bool,bool,bool,bool,bool)", - 0x71C95899: "expectEmitAnonymous(bool,bool,bool,bool,bool,address)", 0x6D016688: "expectSafeMemory(uint64,uint64)", 0x05838BF4: "expectSafeMemoryCall(uint64,uint64)", 0x6248BE1F: "foundryVersionAtLeast(string)", @@ -1552,24 +1544,6 @@ def wrapper(*args, **kwargs): 0xC1ADBBFF: "expectCall(address,bytes,uint64)", 0xF30C7BA3: "expectCall(address,uint256,bytes)", 0xA2B1A1AE: "expectCall(address,uint256,bytes,uint64)", - 0x440ED10D: "expectEmit()", - 0x86B9620D: "expectEmit(address)", - 0x491CC7C2: "expectEmit(bool,bool,bool,bool)", - 0x81BAD6F3: "expectEmit(bool,bool,bool,bool,address)", - 0x11FB5B9C: "expectPartialRevert(bytes4)", - 0x51AA008A: "expectPartialRevert(bytes4,address)", - 0xF4844814: "expectRevert()", - 0xD814F38A: "expectRevert(address)", - 0x1FF5F952: "expectRevert(address,uint64)", - 0xF28DCEB3: "expectRevert(bytes)", - 0x61EBCF12: "expectRevert(bytes,address)", - 0xD345FB1F: "expectRevert(bytes,address,uint64)", - 0x4994C273: "expectRevert(bytes,uint64)", - 0xC31EB0E0: "expectRevert(bytes4)", - 0x260BC5DE: "expectRevert(bytes4,address)", - 0xB0762D73: "expectRevert(bytes4,address,uint64)", - 0xE45CA72D: "expectRevert(bytes4,uint64)", - 0x4EE38244: "expectRevert(uint64)", 0x65BC9481: "accesses(address)", 0xAFC98040: "broadcast()", 0xE6962CDB: "broadcast(address)", diff --git a/tests/expected/all.json b/tests/expected/all.json index 0b57ff8d..891d29eb 100644 --- a/tests/expected/all.json +++ b/tests/expected/all.json @@ -1026,6 +1026,323 @@ "num_bounded_loops": null } ], + "test/Expect.t.sol:ExpectTest": [ + { + "name": "check_expectEmit(uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectEmit_anonymous(uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectEmit_count(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectEmit_count_fail(uint256)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectEmit_emitter(uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectEmit_no_emit_fail(uint256,uint256)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectEmit_ordered(uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectEmit_pending_fail(uint256,uint256)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectEmit_skip_data(uint256,uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectEmit_wrong_data_fail(uint256,uint256,uint256)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectEmit_wrong_emitter_fail(uint256,uint256)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectEmit_wrong_order_fail(uint256,uint256)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectEmit_wrong_topic_fail(uint256,uint256)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectPartialRevert(address)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectRevert_any(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectRevert_any_fail(uint256)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectRevert_count()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectRevert_count_fail()", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectRevert_count_zero()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectRevert_count_zero_fail()", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectRevert_create()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectRevert_custom_error(address)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectRevert_custom_error_symbolic_fail(address,address)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectRevert_no_revert_fail()", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectRevert_pending_fail()", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectRevert_returns_success()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectRevert_reverter()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectRevert_reverter_fail()", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectRevert_selector()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectRevert_selector_with_args_fail(address)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectRevert_state_is_reverted(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectRevert_string(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectRevert_string_bytes(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectRevert_with_prank()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_expectRevert_wrong_string_fail(uint256)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], "test/Extcodehash.t.sol:ExtcodehashTest": [ { "name": "check_extcodehash_a1_eq_a2()", diff --git a/tests/regression/test/Expect.t.sol b/tests/regression/test/Expect.t.sol new file mode 100644 index 00000000..f3164f13 --- /dev/null +++ b/tests/regression/test/Expect.t.sol @@ -0,0 +1,315 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import "forge-std/Test.sol"; + +contract Target { + error Unauthorized(address who); + error Plain(); + + event Deposit(address indexed from, uint256 indexed id, uint256 amount); + event Other(uint256 x); + event Anon(uint256 indexed a, uint256 b) anonymous; + + uint256 public total; + + function requireLt(uint256 x, uint256 bound) external pure returns (uint256) { + require(x < bound, "too big"); + return x; + } + + function onlyOwner(address caller) external pure { + if (caller != address(0xbeef)) revert Unauthorized(caller); + } + + function plain(bool fail) external pure { + if (fail) revert Plain(); + } + + function ok() external pure returns (uint256) { + return 42; + } + + function deposit(uint256 id, uint256 amount) external { + total += amount; + emit Other(amount); + emit Deposit(msg.sender, id, amount); + } + + function anon(uint256 a, uint256 b) external { + emit Anon(a, b); + } + + function depositTwice(uint256 amount) external { + emit Other(amount); + emit Other(amount); + } +} + +contract Inner { + function boom() external pure { + revert("inner"); + } +} + +contract Outer { + Inner public inner = new Inner(); + + function callInner() external view { + inner.boom(); + } +} + +contract Reverting { + constructor(bool fail) { + require(!fail, "ctor"); + } +} + +/// @dev overloads with a count, missing from the pinned forge-std +interface VmCount { + function expectRevert(uint64 count) external; + function expectRevert(bytes4 revertData, uint64 count) external; + function expectEmit(uint64 count) external; +} + +/// @dev regression tests for halmos issue #409: vm.expectRevert / vm.expectEmit +contract ExpectTest is Test { + Target target; + Outer outer; + VmCount vmc = VmCount(address(vm)); + + event Deposit(address indexed from, uint256 indexed id, uint256 amount); + event Other(uint256 x); + event Anon(uint256 indexed a, uint256 b) anonymous; + + function setUp() public { + target = new Target(); + outer = new Outer(); + } + + // + // expectRevert + // + + function check_expectRevert_any(uint256 x) public { + vm.assume(x >= 10); + vm.expectRevert(); + target.requireLt(x, 10); + } + + function check_expectRevert_any_fail(uint256 x) public { + // fails for x < 10 + vm.expectRevert(); + target.requireLt(x, 10); + } + + function check_expectRevert_string(uint256 x) public { + vm.assume(x >= 10); + vm.expectRevert("too big"); + target.requireLt(x, 10); + } + + function check_expectRevert_string_bytes(uint256 x) public { + vm.assume(x >= 10); + vm.expectRevert(abi.encodeWithSignature("Error(string)", "too big")); + target.requireLt(x, 10); + } + + function check_expectRevert_wrong_string_fail(uint256 x) public { + vm.assume(x >= 10); + vm.expectRevert("wrong message"); + target.requireLt(x, 10); + } + + function check_expectRevert_custom_error(address who) public { + vm.assume(who != address(0xbeef)); + vm.expectRevert(abi.encodeWithSelector(Target.Unauthorized.selector, who)); + target.onlyOwner(who); + } + + function check_expectRevert_custom_error_symbolic_fail(address who, address expected) public { + // fails when who != expected + vm.assume(who != address(0xbeef)); + vm.expectRevert(abi.encodeWithSelector(Target.Unauthorized.selector, expected)); + target.onlyOwner(who); + } + + function check_expectRevert_selector() public { + vm.expectRevert(Target.Plain.selector); + target.plain(true); + } + + function check_expectRevert_selector_with_args_fail(address who) public { + // the full revert data is compared: the arguments are missing + vm.assume(who != address(0xbeef)); + vm.expectRevert(Target.Unauthorized.selector); + target.onlyOwner(who); + } + + function check_expectPartialRevert(address who) public { + vm.assume(who != address(0xbeef)); + vm.expectPartialRevert(Target.Unauthorized.selector); + target.onlyOwner(who); + } + + function check_expectRevert_no_revert_fail() public { + vm.expectRevert(); + target.plain(false); + } + + function check_expectRevert_returns_success() public { + // the call is reported as successful to the caller + vm.expectRevert(); + (bool success,) = address(target).call(abi.encodeCall(Target.plain, (true))); + assertTrue(success); + } + + function check_expectRevert_state_is_reverted(uint256 amount) public { + uint256 before = target.total(); + vm.expectRevert(); + this.depositAndRevert(amount); + assertEq(target.total(), before); + } + + function depositAndRevert(uint256 amount) external { + target.deposit(1, amount); + revert("rollback"); + } + + function check_expectRevert_with_prank() public { + // cheatcode calls in between don't consume the expectation + vm.expectRevert(abi.encodeWithSelector(Target.Unauthorized.selector, address(this))); + vm.prank(address(0x1234)); + target.onlyOwner(address(this)); + } + + function check_expectRevert_reverter() public { + vm.expectRevert("inner", address(outer.inner())); + outer.callInner(); + } + + function check_expectRevert_reverter_fail() public { + vm.expectRevert(address(outer)); + outer.callInner(); + } + + function check_expectRevert_count() public { + vmc.expectRevert(Target.Plain.selector, 2); + target.plain(true); + target.plain(true); + } + + function check_expectRevert_count_fail() public { + vmc.expectRevert(Target.Plain.selector, 2); + target.plain(true); + target.plain(false); + } + + function check_expectRevert_count_zero() public { + vmc.expectRevert(0); + target.plain(false); + } + + function check_expectRevert_count_zero_fail() public { + vmc.expectRevert(0); + target.plain(true); + } + + function check_expectRevert_create() public { + vm.expectRevert(bytes("ctor")); + new Reverting(true); + } + + function check_expectRevert_pending_fail() public { + // the expected call never happens + vm.expectRevert(); + } + + // + // expectEmit + // + + function check_expectEmit(uint256 id, uint256 amount) public { + vm.expectEmit(); + emit Deposit(address(this), id, amount); + target.deposit(id, amount); + } + + function check_expectEmit_wrong_data_fail(uint256 id, uint256 amount, uint256 other) public { + // fails when other != amount + vm.expectEmit(); + emit Deposit(address(this), id, other); + target.deposit(id, amount); + } + + function check_expectEmit_skip_data(uint256 id, uint256 amount, uint256 other) public { + vm.expectEmit(true, true, false, false); + emit Deposit(address(this), id, other); + target.deposit(id, amount); + } + + function check_expectEmit_wrong_topic_fail(uint256 id, uint256 amount) public { + vm.expectEmit(true, false, false, true); + emit Deposit(address(0xdead), id, amount); + target.deposit(id, amount); + } + + function check_expectEmit_emitter(uint256 id, uint256 amount) public { + vm.expectEmit(address(target)); + emit Deposit(address(this), id, amount); + target.deposit(id, amount); + } + + function check_expectEmit_wrong_emitter_fail(uint256 id, uint256 amount) public { + vm.expectEmit(address(outer)); + emit Deposit(address(this), id, amount); + target.deposit(id, amount); + } + + function check_expectEmit_ordered(uint256 id, uint256 amount) public { + vm.expectEmit(); + emit Other(amount); + vm.expectEmit(); + emit Deposit(address(this), id, amount); + target.deposit(id, amount); + } + + function check_expectEmit_wrong_order_fail(uint256 id, uint256 amount) public { + vm.expectEmit(); + emit Deposit(address(this), id, amount); + vm.expectEmit(); + emit Other(amount); + target.deposit(id, amount); + } + + function check_expectEmit_anonymous(uint256 a, uint256 b) public { + vm.expectEmitAnonymous(); + emit Anon(a, b); + target.anon(a, b); + } + + function check_expectEmit_count(uint256 amount) public { + vmc.expectEmit(2); + emit Other(amount); + target.depositTwice(amount); + } + + function check_expectEmit_count_fail(uint256 amount) public { + vmc.expectEmit(1); + emit Other(amount); + target.depositTwice(amount); + } + + function check_expectEmit_no_emit_fail(uint256 id, uint256 amount) public { + // the expected event is not emitted before the call + vm.expectEmit(); + target.deposit(id, amount); + } + + function check_expectEmit_pending_fail(uint256 id, uint256 amount) public { + // the expected call never happens + vm.expectEmit(); + emit Deposit(address(this), id, amount); + } +} diff --git a/tests/regression/test/SvmCheatCode.t.sol b/tests/regression/test/SvmCheatCode.t.sol index 1efb8339..e7754027 100644 --- a/tests/regression/test/SvmCheatCode.t.sol +++ b/tests/regression/test/SvmCheatCode.t.sol @@ -279,7 +279,7 @@ contract SvmCheatCodeTest is SymTest, Test { function check_unsupported_cheatcode_fail() public { // expected to fail with unknown cheatcode - vm.expectRevert("will revert"); + vm.expectCall(address(0xdead), ""); } function check_env_missing_fail() public { From 85a963680c01281b7f3cde9bd440d46009649100 Mon Sep 17 00:00:00 2001 From: Hugo DE SA PEREIRA PINTO Date: Wed, 23 Sep 2026 07:32:15 +0000 Subject: [PATCH 31/46] feat: run tests from every setUp() path (halmos#186) setUp() with more than one feasible successful path used to abort with "Multiple paths were found in setUp()". - setup() now returns every path that is not provably infeasible - all of them become the initial frontier states, so each regular test and each invariant test sequence is explored from every setup state; counterexamples include the symbolic values selecting the setup state - the invariant testing context (targets, senders, selectors) is read from the first setup state - the number of setup states is reported with -v - add tests/regression/test/SetupMulti.t.sol (regular, setUpSymbolic and invariant tests) --- CHANGELOG.md | 3 + docs/getting-started.md | 2 + src/dolmos/__main__.py | 54 +++++++++----- tests/expected/all.json | 85 ++++++++++++++++++++++ tests/regression/test/SetupMulti.t.sol | 98 ++++++++++++++++++++++++++ 5 files changed, 223 insertions(+), 19 deletions(-) create mode 100644 tests/regression/test/SetupMulti.t.sol diff --git a/CHANGELOG.md b/CHANGELOG.md index 46c690a5..bba5c970 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ All notable changes to dolmos are documented here. The format follows with foundry's semantics; symbolic revert data and event fields are matched symbolically and a counterexample is reported when an expectation can be violated (halmos#409) +- multiple setup states: when `setUp()` has several feasible successful paths, + each test and each invariant test sequence is run from every one of them, + instead of failing with "Multiple paths were found in setUp()" (halmos#186) ### Fixed diff --git a/docs/getting-started.md b/docs/getting-started.md index 4b38fc37..7fca3510 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -56,6 +56,8 @@ By using the symbolic initial supply, you can check if the given tests pass for - The current list of available cheatcodes can be found [here][halmos-cheatcodes-list]. +- `setUp()` may branch on symbolic values (or revert on some of them): every feasible successful path becomes a separate setup state, and each test (and each invariant test sequence) is run from each of them. Counterexamples include the symbolic values that select the setup state. Run with `-v` to display the number of setup states. + [halmos-cheatcodes-list]: ## 2. Write symbolic tests diff --git a/src/dolmos/__main__.py b/src/dolmos/__main__.py index 8083432e..148436d9 100644 --- a/src/dolmos/__main__.py +++ b/src/dolmos/__main__.py @@ -380,7 +380,13 @@ def deploy_test(ctx: FunctionContext, sevm: SEVM) -> Exec: return ex -def setup(ctx: FunctionContext) -> Exec: +def setup(ctx: FunctionContext) -> list[Exec]: + """ + Deploys the test contract and runs its setUp function. + + Returns every feasible successful setup path: when setUp branches (e.g. on + symbolic values), each test is run from each of the resulting states. + """ setup_timer = NamedTimer("setup") setup_timer.create_subtimer("decode") @@ -394,7 +400,7 @@ def setup(ctx: FunctionContext) -> Exec: if not setup_sig: if args.statistics: print(setup_timer.report()) - return setup_ex + return [setup_ex] # TODO: dyn_params may need to be passed to mk_calldata in run() calldata, dyn_params = mk_calldata(ctx.contract_ctx.abi, setup_info, args) @@ -449,6 +455,7 @@ def setup(ctx: FunctionContext) -> Exec: case [(ex, _)]: setup_exs.append(ex) case _: + # keep every path that is not provably infeasible for path_id, (ex, query) in enumerate(setup_exs_no_error): path_ctx = PathContext( args=args, @@ -459,20 +466,24 @@ def setup(ctx: FunctionContext) -> Exec: solver_output = solve_low_level(path_ctx) if solver_output.result != unsat: setup_exs.append(ex) - if len(setup_exs) > 1: - break - match len(setup_exs): - case 0: - raise DolmosException(f"No successful path found in {setup_sig}") - case n if n > 1: - debug("\n".join(map(str, setup_exs))) - raise DolmosException(f"Multiple paths were found in {setup_sig}") + if not setup_exs: + raise DolmosException(f"No successful path found in {setup_sig}") - [setup_ex] = setup_exs + if len(setup_exs) > 1: + msg = ( + f"{setup_sig}: {len(setup_exs)} setup states found; " + "each test will be run from each of them" + ) + if args.verbose >= 1: + print(msg) + else: + debug(msg) if args.print_setup_states: - print(setup_ex) + for path_id, ex in enumerate(setup_exs): + print(f"# setup state {path_id}") + print(ex) if sevm.logs.bounded_loops: warn_code( @@ -484,7 +495,7 @@ def setup(ctx: FunctionContext) -> Exec: if args.statistics: print(setup_timer.report()) - return setup_ex + return setup_exs def is_global_fail_set(context: CallContext) -> bool: @@ -1650,8 +1661,9 @@ def run_contract(ctx: ContractContext) -> list[TestResult]: ) dolmos.traces.config_context.set(setup_config) - setup_ex = setup(setup_ctx) - setup_ex.path_slice() + setup_exs = setup(setup_ctx) + for setup_ex in setup_exs: + setup_ex.path_slice() except Exception as err: error(f"{setup_info.sig} failed: {type(err).__name__}: {err}") if args.debug: @@ -1662,11 +1674,15 @@ def run_contract(ctx: ContractContext) -> list[TestResult]: return [] - # initialize the frontier and visited states using the initial setup state - ctx.frontier_states[0] = [setup_ex] - ctx.visited.add(get_state_id(setup_ex)) + # initialize the frontier and visited states using the initial setup states. + # every test (and every invariant test sequence) starts from each of them. + ctx.frontier_states[0] = setup_exs + for setup_ex in setup_exs: + ctx.visited.add(get_state_id(setup_ex)) - test_results = run_tests(ctx, setup_ex, ctx.funsigs) + # the invariant testing context (target contracts, senders, selectors) + # is read from the first setup state + test_results = run_tests(ctx, setup_exs[0], ctx.funsigs) # reset any remaining solver states from the default context reset(setup_solver) diff --git a/tests/expected/all.json b/tests/expected/all.json index 891d29eb..5791454d 100644 --- a/tests/expected/all.json +++ b/tests/expected/all.json @@ -4265,6 +4265,91 @@ "num_bounded_loops": null } ], + "test/SetupMulti.t.sol:SetupMultiTest": [ + { + "name": "check_fee_bounded()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_fee_small_fail()", + "exitcode": 1, + "num_models": 2, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_mode_consistent()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_not_paused_fail()", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_setFee(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "invariant_fee_bounded()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "invariant_fee_unchanged_fail()", + "exitcode": 1, + "num_models": 4, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/SetupMulti.t.sol:SetupSymbolicMultiTest": [ + { + "name": "check_value()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_value_fail()", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], "test/SetupPlus.t.sol:SetupPlusTest": [ { "name": "check_Setup()", diff --git a/tests/regression/test/SetupMulti.t.sol b/tests/regression/test/SetupMulti.t.sol new file mode 100644 index 00000000..cde269b9 --- /dev/null +++ b/tests/regression/test/SetupMulti.t.sol @@ -0,0 +1,98 @@ +// 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 Vault { + uint256 public fee; + bool public paused; + + constructor(uint256 _fee, bool _paused) { + fee = _fee; + paused = _paused; + } + + function setFee(uint256 _fee) external { + require(!paused, "paused"); + require(_fee <= 1000, "fee too high"); + fee = _fee; + } +} + +/// @dev regression tests for halmos issue #186: setUp() with several paths +/// used to fail with "Multiple paths were found in setUp()" +contract SetupMultiTest is Test, SymTest { + Vault vault; + uint256 mode; + + function setUp() public { + // symbolic branching in setUp: three setup states + mode = svm.createUint256("mode"); + if (mode == 0) { + vault = new Vault(10, false); + } else if (mode == 1) { + vault = new Vault(500, false); + } else { + vault = new Vault(1000, true); + } + targetContract(address(vault)); + } + + function check_fee_bounded() public view { + // holds in every setup state + assertLe(vault.fee(), 1000); + } + + function check_fee_small_fail() public view { + // only holds in the first setup state: counterexample with mode != 0 + assertLe(vault.fee(), 10); + } + + function check_not_paused_fail() public view { + // violated in the third setup state only + assertFalse(vault.paused()); + } + + function check_setFee(uint256 newFee) public { + vault.setFee(newFee); + // setFee only succeeds when not paused, and keeps the fee bounded + assertFalse(vault.paused()); + assertLe(vault.fee(), 1000); + } + + function invariant_fee_bounded() public view { + // explored from each setup state, including through setFee() calls + assertLe(vault.fee(), 1000); + } + + function invariant_fee_unchanged_fail() public view { + // setFee() can change the fee in the unpaused setup states + assertTrue(vault.fee() == 10 || vault.fee() == 500 || vault.fee() == 1000); + } + + function check_mode_consistent() public view { + // the test runs against the matching state for each setUp path + if (mode == 0) assertEq(vault.fee(), 10); + else if (mode == 1) assertEq(vault.fee(), 500); + else assertEq(vault.fee(), 1000); + } +} + +/// @dev setUpSymbolic with several successful paths +contract SetupSymbolicMultiTest { + uint256 public value; + + function setUpSymbolic(uint256 x) public { + if (x > 10) value = 1; + else value = 2; + } + + function check_value() public view { + assert(value == 1 || value == 2); + } + + function check_value_fail() public view { + assert(value == 1); + } +} From 59bb438b60ce10f88081c25971deca6e9cd05547 Mon Sep 17 00:00:00 2001 From: Hugo DE SA PEREIRA PINTO Date: Wed, 23 Sep 2026 15:13:43 +0200 Subject: [PATCH 32/46] Adding first export to passtel --- CHANGELOG.md | 6 + README.md | 2 + docs/loop-termination.md | 217 ++++++++++ docs/warnings.md | 3 + src/dolmos/__main__.py | 3 + src/dolmos/config.py | 23 + src/dolmos/lasso.py | 886 +++++++++++++++++++++++++++++++++++++++ src/dolmos/sevm.py | 288 ++++++++++++- tests/test_lasso.py | 139 ++++++ 9 files changed, 1566 insertions(+), 1 deletion(-) create mode 100644 docs/loop-termination.md create mode 100644 src/dolmos/lasso.py create mode 100644 tests/test_lasso.py diff --git a/CHANGELOG.md b/CHANGELOG.md index bba5c970..0ca6d707 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,12 @@ All notable changes to dolmos are documented here. The format follows - multiple setup states: when `setUp()` has several feasible successful paths, each test and each invariant test sequence is run from every one of them, instead of failing with "Multiple paths were found in setUp()" (halmos#186) +- experimental: `--dump-lassos DIR` exports each loop that reaches the unrolling + bound as a lasso program, and `--pasttel BINARY` analyses it with PaSTTeL + (termination proof with a ranking function, or non-termination). Lassos use + a small-constants encoding by default, because PaSTTeL computes with doubles + and is unsound on 2^256 constants; `--lasso-exact-constants` gives the exact + encoding. See docs/loop-termination.md ### Fixed diff --git a/README.md b/README.md index 0c758f41..389f6784 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ counterexample. Start with the [getting started guide](https://github.com/Dowsers/dolmos/blob/main/docs/getting-started.md) and the [examples](https://github.com/Dowsers/dolmos/blob/main/examples/README.md). +Loops that reach the unrolling bound can be exported for termination analysis with +PaSTTeL (experimental): see [docs/loop-termination.md](https://github.com/Dowsers/dolmos/blob/main/docs/loop-termination.md). ## Features diff --git a/docs/loop-termination.md b/docs/loop-termination.md new file mode 100644 index 00000000..0668a1e3 --- /dev/null +++ b/docs/loop-termination.md @@ -0,0 +1,217 @@ +# Loop termination analysis with PaSTTeL (experimental) + +dolmos explores loops by unrolling them up to a bound (`--loop`, 2 by default). When a +path would need more iterations, it is dropped and the test reports +`bounds: [...]`: the exploration is incomplete, and nothing is known about the loop +itself. + +With `--dump-lassos`, every loop that reaches the unrolling bound is exported as a +*lasso program*, which [PaSTTeL](https://github.com/Dowsers/PaSTTeL) can analyse to +prove termination (with a ranking function) or non-termination. + +- [Usage](#usage) +- [Reading the results](#reading-the-results) +- [How a loop becomes a lasso](#how-a-loop-becomes-a-lasso) +- [From bitvectors to integers](#from-bitvectors-to-integers) +- [The small-constants encoding (PaSTTeL workaround)](#the-small-constants-encoding-pasttel-workaround) +- [Limitations](#limitations) + +## Usage + +```bash +# export the lassos only +dolmos --dump-lassos lassos/ + +# export and analyse them +dolmos --dump-lassos lassos/ --pasttel /path/to/pasttel [--pasttel-timeout 30] +``` + +| Option | Description | +|---|---| +| `--dump-lassos DIR` | export each loop that reaches the unrolling bound to `DIR` | +| `--pasttel BINARY` | run PaSTTeL on each exported lasso and report the verdict | +| `--pasttel-timeout SECONDS` | time limit per PaSTTeL run (default: 30) | +| `--lasso-exact-constants` | exact wrap-around encoding with 2^256 constants, for tools with exact arithmetic (not PaSTTeL, see below) | + +The option has no effect on the test results: the extraction runs on a separate +solver, and a failed extraction is reported without affecting the test. + +For each loop, three files are written: + +- `__.json`: the lasso, in PaSTTeL's JSON trace format +- `__.meta.json`: which stack slots the program variables + `v0, v1, ...` stand for, the number of body paths, the encoding, and warnings +- `__.pasttel.txt`: PaSTTeL's output (with `--pasttel`) + +and a line is printed after the test result: + +``` +[PASS] check_count_down(uint256) (paths: 5, time: 0.15s, bounds: []) + loop 2129:473 (false branch): TERMINATING [RankingBased(AffineTemplate): v0] -> lassos/check_count_down_2129_473_f.json +``` + +## Reading the results + +- **TERMINATING**, with a ranking function: the loop terminates. This result is + reliable: the lasso over-approximates the behaviours of the loop, and a proof for + more behaviours is a proof for the actual ones. +- **NON-TERMINATING**: PaSTTeL found an infinite execution *of the lasso*. If the lasso + is exact (`"exact": true` in the metadata), the witness is an infinite execution of + the loop. Otherwise the verdict is printed as *unconfirmed*: the witness may rely on + behaviours that the over-approximation introduced. +- **UNKNOWN / TIMEOUT**: no conclusion. +- **Warnings**: + - `storage written in the loop body`: only stack variables are tracked (see + [Limitations](#limitations)). If the loop condition depends on a storage slot + written in the body, the result is not reliable, including a TERMINATING verdict. + - `body path not explored`, `too many body paths`, `step budget exhausted`: the + extraction is incomplete; the result is only indicative. + +## How a loop becomes a lasso + +A lasso is a *stem* (how the execution reaches the loop) followed by a *loop* +transition that repeats forever. dolmos builds both from the symbolic execution: + +1. **Loop variables.** At each visit of a symbolic `JUMPI`, the stack is recorded. + When the unrolling bound is reached, the stack slots that differ from the previous + visit are the loop variables. +2. **Cut point.** The lasso is cut right after the `JUMPI`, on the branch that + stays in the loop. The bound is often reached on a `JUMPI` inside an internal + function that solc generates (e.g. checked arithmetic): any point of the cycle is + a valid cut point, but it must be identified precisely. Like the unrolling + counters, dolmos uses the program counter and the jump destinations on the stack, + plus the stack height to tell apart the call sites of the same internal function. +3. **Generalized iteration.** The loop body is executed once more from a state where + the loop variables are fresh symbols, until the execution is back at the cut + point in the same frame. Each path that comes back *and takes the loop branch + again* is a disjunct of the loop transition. This execution runs on a separate + solver, with its own step budget. +4. **Stem.** The path condition up to the cut point, sliced to the conditions that + relate to the initial values of the loop variables. + +Subterms that don't depend on the loop variables (inputs, values read before the +loop) become constant program variables, shared by the stem and the loop: without +them, `for (i = 0; i < n; i++)` could not be proven, as `n` would change at every +iteration. + +## From bitvectors to integers + +EVM words are 256-bit bitvectors; PaSTTeL works on integers. Every w-bit value +becomes an integer `x` with `0 <= x`, and: + +| Bitvector | Integer encoding | +|---|---| +| `bvadd`, `bvsub`, `bvneg`, `bvmul`/`bvshl` by a constant | exact, with the wrap-around made explicit (see below) | +| `bvudiv`, `bvurem`, `bvlshr` by a constant, `extract`, `concat`, `zero_extend` | exact, with a quotient `q` such that `c*q <= a <= c*q + c - 1` | +| unsigned comparisons, boolean connectives | exact | +| `ite` | a fresh variable and a disjunction (PaSTTeL doesn't parse arithmetic `ite`) | +| anything else (keccak, storage and memory reads, bitwise operations, nonlinear arithmetic...) | a fresh variable: over-approximation | + +## The small-constants encoding (PaSTTeL workaround) + +### The problem + +The natural encoding of a wrap-around is `r = e - 2^256 * k`. It needs 2^256 +constants, and PaSTTeL cannot represent them: it parses and stores numbers as C++ +`double` (`std::stod` in its SMT parser, `double` coefficients in `AffineTerm`), +which are exact for integers up to 2^53 only. 2^256 and 2^256 - 1 become the same +number, and the analysis becomes unsound. In our experiments, PaSTTeL reported a +simple countdown loop as non-terminating, with a witness that is impossible even over +the rationals; it also produced ranking functions with coefficients equal to +`DBL_MAX`, and, worse, a termination proof for a loop that does not terminate (see +[Results](#results)). + +(A second, independent issue: ranking function synthesis works over the rationals. +With a free integer `k`, a fractional `k` allows any result, so the encoding +`r = e - 2^256 * k` loses all precision even with exact arithmetic. The exact +encoding therefore uses one disjunct per possible value of `k`.) + +The proper fix is exact rational arithmetic in PaSTTeL. Until then, dolmos produces +lassos without large constants: this is the default encoding. + +### The encoding + +No literal in the lasso exceeds 2^52 (this is checked before writing the file). + +1. **Wrap-arounds are proven absent when possible.** For each addition, subtraction + or multiplication by a constant, dolmos asks z3, with the bitvector semantics, + whether the operation can wrap around on the path. If it cannot, the result is + the plain integer expression (`x + 1`, `x - 1`), which is exact. + - With checked arithmetic (Solidity >= 0.8), the overflow checks are on the path, + so the proof usually succeeds. + - `x - 1` is compiled as `x + (2^256 - 1)`: an addition of a constant `C >= + 2^255` is handled as a subtraction of `2^256 - C`, with the borrow condition + `x >= 1`. +2. **The prover knows how the iteration was entered.** An increment `i + 1` only + wraps around if `i = 2^256 - 1`, which is excluded by the loop condition `i < n` + checked *before* the iteration, i.e. at the end of the previous one. The prover is + given this fact, as a disjunction over the body paths with renamed variables. It + is valid because the unrolling bound was reached (so there was a previous + iteration), and only used when the body paths are complete. +3. **Facts about the inputs are used.** The path conditions of the stem are about + input symbols, which never change: they hold at every iteration. For instance, + `vm.assume(n < 1000)` lets the prover show that `i += 3` doesn't wrap while + `i < n`. +4. **Otherwise, a sound over-approximation without large constants:** + - addition, multiplication (the result can only wrap downwards): `0 <= r <= e` + - subtraction (the result can only wrap upwards): + `(e >= 0 and r = e) or e < 0`, where `r` is unconstrained in the second case + - truncation (`extract`, masks): same as addition + - division or remainder by a large constant: `0 <= q <= a` +5. **Large literals become constant program variables** (`c0, c1, ...`), constrained + by their order (`0 <= c0 < c1 < ...`): comparisons such as `x < 2^200` keep their + structure. +6. **Upper bounds `x < 2^256` are dropped** (only `0 <= x` is kept): an + over-approximation. +7. **Signed comparisons** are exact when both operands are provably below 2^255 + (then they are unsigned comparisons); otherwise they are over-approximated. + +The metadata records the encoding and whether the lasso is exact +(`"exact": false` as soon as one over-approximation was used). + +### Results + +On a set of test loops (countdown, count-up, `unchecked` step of 3, loops leaving +through a wrap-around): + +| Loop | exact encoding (`--lasso-exact-constants`) | small-constants encoding (default) | +|---|---|---| +| `while (x > 0) x -= 1;` | TERMINATING, coefficients ~2^251 (unreliable) | TERMINATING, rank `v0` | +| `for (i = 0; i < n; i++) s += 1;` | TERMINATING, huge coefficients (unreliable) | TERMINATING, rank `5·k0 + v0 − 3·v1 − 3` | +| `while (i < n) unchecked { i += 3; }` (`n < 1000`) | TERMINATING, huge coefficients (unreliable) | TERMINATING, rank `8·k0 − 2·v0 − 27` | +| `while (x != 0) { unchecked { x -= 2; } if (x > 1000) break; }` | TERMINATING, rank `10·v0 − 1` | TERMINATING, rank `10·v0 + 19` | +| `while (x != 0) unchecked { x -= 2; }` (non-terminating for odd `x`) | **TERMINATING (wrong)** | NON-TERMINATING, unconfirmed (correct) | + +The fourth loop terminates: for odd `x`, `x - 2` eventually wraps around to +`2^256 - 1 > 1000`. An earlier version of the extraction, with the exact encoding, +reported it and the countdown loop as non-terminating. + +On the regression suite, the 20 loops reaching the bound are all extracted; the +test results are unchanged. + +### Exact encoding + +`--lasso-exact-constants` produces the exact encoding (2^256 constants, one +disjunct per wrap-around case). It is meant for termination tools with exact +arithmetic; with the current PaSTTeL, its results are unreliable. + +## Limitations + +- **Only stack variables are tracked.** Storage and memory are not generalized: + values read from them are either constants (if they don't depend on the loop + variables) or fresh values. Loops whose body writes to storage are flagged. + Loops that index memory with the loop counter (`arr[i]` on a memory array) often + cannot be extracted, as dolmos doesn't support symbolic memory offsets. +- **Non-termination is not confirmed.** A NON-TERMINATING verdict on an + over-approximated lasso would need to be replayed with the bitvector semantics; + this is not implemented yet. +- **Loops whose condition becomes constant are not exported.** dolmos only applies + the unrolling bound to loops whose condition is symbolic. A loop that makes no + progress, like `while (x > y) {}`, has a condition that becomes known after the + first iteration: dolmos keeps unrolling it forever, and the bound, hence the export, + is never reached. This is an existing dolmos limitation. +- **One cut point per loop and branch** is exported per test; the lasso describes + the loop from that point, with the paths found from the state where the bound was + reached. +- The ranking function proves termination, but it is not used yet to raise the + unrolling bound of the loop. diff --git a/docs/warnings.md b/docs/warnings.md index c56f7465..f86550f6 100644 --- a/docs/warnings.md +++ b/docs/warnings.md @@ -52,3 +52,6 @@ Some paths were cut at the loop unrolling bound (`--loop`), so they have not been fully explored and bugs beyond that bound can be missed. Raise `--loop` (globally or with a `@custom:dolmos --loop N` annotation) if the loops in question are bounded by a small constant. + +To find out whether these loops terminate, export them with `--dump-lassos` and +analyse them with PaSTTeL: see [loop-termination.md](loop-termination.md). diff --git a/src/dolmos/__main__.py b/src/dolmos/__main__.py index 148436d9..e62ea76f 100644 --- a/src/dolmos/__main__.py +++ b/src/dolmos/__main__.py @@ -1325,6 +1325,9 @@ def run_test(ctx: FunctionContext) -> TestResult: ) debug("\n".join(jumpid_str(x) for x in logs.bounded_loops)) + for lasso_result in logs.lassos: + print(f" {lasso_result.summary()}") + # return test result num_cexes = len(ctx.valid_counterexamples) + len(ctx.invalid_counterexamples) if args.minimal_json_output: diff --git a/src/dolmos/config.py b/src/dolmos/config.py index 6bb84cd1..bb88ea8e 100644 --- a/src/dolmos/config.py +++ b/src/dolmos/config.py @@ -316,6 +316,29 @@ class Config: metavar="MAX_BOUND", ) + dump_lassos: str = arg( + help="when the loop unrolling bound is reached, export the loop as a lasso program (PaSTTeL JSON format) in the given directory", + global_default=None, + metavar="LASSO_DIR", + ) + + pasttel: str = arg( + help="path to the PaSTTeL binary: run the termination analysis on each exported lasso (requires --dump-lassos)", + global_default=None, + metavar="PASTTEL_BINARY", + ) + + lasso_exact_constants: bool = arg( + help="encode wrap-arounds exactly in exported lassos, with 2^256 constants (for termination tools with exact arithmetic; PaSTTeL needs the default small-constants encoding)", + global_default=False, + ) + + pasttel_timeout: int = arg( + help="time limit in seconds for each PaSTTeL run", + global_default=30, + metavar="SECONDS", + ) + width: int = arg( help="set the max number of paths; 0 means unlimited", global_default=0, diff --git a/src/dolmos/lasso.py b/src/dolmos/lasso.py new file mode 100644 index 00000000..7ca56fc1 --- /dev/null +++ b/src/dolmos/lasso.py @@ -0,0 +1,886 @@ +# SPDX-License-Identifier: AGPL-3.0 + +""" +Export of bounded loops as lasso programs, for termination analysis with PaSTTeL. + +When the loop unrolling bound (--loop) is reached on a symbolic JUMPI, dolmos can +export the loop as a *lasso*: a stem (how the execution reaches the loop) and a +loop transition (one iteration of the body), in PaSTTeL's JSON format +(https://github.com/Dowsers/PaSTTeL). + +Extraction (see SEVM.extract_lasso) +- the loop variables are the stack slots that change between two visits +- the lasso is cut right after the JUMPI, on the branch that stays in the loop +- the body is re-executed once from a generalized state (fresh loop variables), + on a separate solver; each path that comes back and takes the loop branch + again is a disjunct of the loop transition + +Translation (Translator): bitvectors become non-negative integers. Exact where +possible, over-approximated otherwise (fresh variables), so that termination +proofs stay valid. + +By default, lassos use the *small-constants encoding*: no literal exceeds 2^52, +because PaSTTeL computes with doubles and is unsound on 2^256 constants. +Wrap-arounds are proven absent with z3 (bitvector semantics) when possible, and +over-approximated without large constants otherwise. + +See docs/loop-termination.md for details, soundness and limitations. +""" + +import json +import os +import re +import subprocess +from dataclasses import dataclass, field + +from z3 import ( + UGE, + ULT, + Z3_OP_AND, + Z3_OP_BADD, + Z3_OP_BAND, + Z3_OP_BLSHR, + Z3_OP_BMUL, + Z3_OP_BNEG, + Z3_OP_BNUM, + Z3_OP_BSHL, + Z3_OP_BSUB, + Z3_OP_BUDIV, + Z3_OP_BUDIV_I, + Z3_OP_BUREM, + Z3_OP_BUREM_I, + Z3_OP_CONCAT, + Z3_OP_DISTINCT, + Z3_OP_EQ, + Z3_OP_EXTRACT, + Z3_OP_FALSE, + Z3_OP_IFF, + Z3_OP_IMPLIES, + Z3_OP_ITE, + Z3_OP_NOT, + Z3_OP_OR, + Z3_OP_SGEQ, + Z3_OP_SGT, + Z3_OP_SLEQ, + Z3_OP_SLT, + Z3_OP_TRUE, + Z3_OP_UGEQ, + Z3_OP_UGT, + Z3_OP_ULEQ, + Z3_OP_ULT, + Z3_OP_UNINTERPRETED, + Z3_OP_XOR, + Z3_OP_ZERO_EXT, + And, + BitVec, + BitVecRef, + BitVecVal, + BoolRef, + ExprRef, + LShR, + Not, + Or, + Solver, + Sum, + ZeroExt, + is_bool, + is_bv, + is_bv_value, + is_false, + is_true, + simplify, + substitute, + unsat, +) +from z3.z3util import get_vars + +# upper bound on the paths of the generalized iteration +MAX_BODY_PATHS = 32 + +# upper bound on the number of instructions executed by the generalized iteration +MAX_BODY_STEPS = 200_000 + +LOOP_VAR_PREFIX = "lasso_v" + +# wrap-arounds with at most this many cases are encoded as disjunctions +MAX_WRAP_CASES = 4 + +# PaSTTeL stores numbers as doubles: integers are exact up to 2^53 +MAX_SMALL_CONSTANT = 2**52 + + +# +# snapshots and results +# + + +@dataclass(frozen=True) +class LoopSnapshot: + stack: tuple # stack below the JUMPI operands + cond: BoolRef # branching condition + + +@dataclass +class LassoResult: + test: str + jumpid: str + direction: bool + path: str | None = None # dumped JSON file + error: str | None = None # extraction failure + warnings: list[str] = field(default_factory=list) + verdict: str | None = None # PaSTTeL verdict + proof: str | None = None + approximate: bool = False # the lasso over-approximates the loop + + def summary(self) -> str: + where = f"loop {self.jumpid} ({'true' if self.direction else 'false'} branch)" + if self.error: + return f"{where}: lasso not extracted: {self.error}" + text = f"{where}: {self.verdict or 'lasso dumped'}" + if self.verdict == "NON-TERMINATING" and (self.approximate or self.warnings): + # only termination proofs survive over-approximation + text += " (unconfirmed: the lasso over-approximates the loop)" + if self.proof: + text += f" [{self.proof}]" + if self.warnings: + text += f" (warning: {'; '.join(self.warnings)})" + if self.path: + text += f" -> {self.path}" + return text + + +class LassoError(Exception): + pass + + +# +# BV -> Int translation +# + + +def is_loop_var(e: ExprRef) -> bool: + return ( + e.num_args() == 0 + and e.decl().kind() == Z3_OP_UNINTERPRETED + and e.decl().name().startswith(LOOP_VAR_PREFIX) + ) + + +def depends_on_loop_vars(e: ExprRef) -> bool: + return any(is_loop_var(v) for v in get_vars(e)) + + +class WrapProver: + """ + Proves, with the bitvector semantics, that an operation cannot wrap around + under the conditions of a path. Used by the small-constants encoding. + + The conditions of a generalized iteration mention the input symbols (via the + stem) and the fresh loop variables (via the body). Input symbols never + change, so facts about them hold at every iteration: using them is sound. + """ + + def __init__(self, conditions: list[BoolRef], timeout_ms: int): + self.solver = Solver() + self.solver.set(timeout=timeout_ms) + self.solver.add(*conditions) + self.memo: dict[int, bool] = {} + + def valid(self, cond: BoolRef) -> bool: + cond = simplify(cond) + if is_true(cond): + return True + if is_false(cond): + return False + key = cond.get_id() + if key not in self.memo: + self.solver.push() + self.solver.add(Not(cond)) + self.memo[key] = self.solver.check() == unsat + self.solver.pop() + return self.memo[key] + + +class Registry: + """State shared by all the translation contexts of one lasso.""" + + def __init__(self): + self.aux_counter = 0 + # loop-invariant opaque terms -> (program variable name, width, term) + self.invariants: dict[int, tuple[str, int, ExprRef]] = {} + # large literals (small-constants encoding) -> program variable name + self.big_constants: dict[int, str] = {} + # set when the translation over-approximates the bitvector semantics + self.approximate = False + + def fresh(self, prefix: str) -> str: + self.aux_counter += 1 + return f"{prefix}{self.aux_counter}" + + def invariant(self, e: ExprRef) -> str: + key = e.get_id() + if key not in self.invariants: + width = e.size() if is_bv(e) else 1 + self.invariants[key] = (f"k{len(self.invariants)}", width, e) + return self.invariants[key][0] + + def big_constant(self, value: int) -> str: + if value not in self.big_constants: + self.big_constants[value] = f"c{len(self.big_constants)}" + return self.big_constants[value] + + +class Translator: + """ + Translates bitvector formulas to linear integer SMT-LIB terms. + + One translator per transition disjunct: auxiliary variables and their + constraints are local to it. + + Two encodings: + - exact (prover is None): wrap-arounds are encoded exactly, with 2^w + constants. Suitable for tools with exact arithmetic. + - small constants (with a prover): no literal exceeds MAX_SMALL_CONSTANT, + because PaSTTeL represents numbers as doubles (exact up to 2^53 only). + See docs/loop-termination.md. + """ + + def __init__( + self, + registry: Registry, + names: dict[str, str], + stem: bool, + prover: WrapProver | None = None, + ): + self.reg = registry + self.names = names # loop variable name -> SSA name + self.stem = stem + self.prover = prover + self.aux: list[str] = [] + self.constraints: list[str] = [] + self.memo: dict[int, str] = {} + + @property + def small(self) -> bool: + return self.prover is not None + + # helpers + + def _aux(self, width: int | None) -> str: + name = self.reg.fresh("aux") + self.aux.append(name) + if width is not None: + self.constraints.append(f"(<= 0 {name})") + upper = 2**width + if not self.small or upper <= MAX_SMALL_CONSTANT: + self.constraints.append(f"(< {name} {upper})") + return name + + def _num(self, value: int) -> str: + """A literal; large ones become constant program variables if needed.""" + if self.small and value > MAX_SMALL_CONSTANT: + self.reg.approximate = True + name = self.reg.big_constant(value) + return f"{name}_s" if self.stem else name + return str(value) + + def _wrap( + self, + expr: str, + width: int, + k_min: int, + k_max: int | None, + no_wrap: BoolRef | None = None, + ) -> str: + """ + expr mod 2^width, where expr - r = 2^width * k with k in [k_min, k_max]. + + `no_wrap` is the bitvector condition under which k = 0. + """ + m = 2**width + + if self.small: + # k = 0 proven: the result is the integer expression itself + if no_wrap is not None and self.prover.valid(no_wrap): + return expr + self.reg.approximate = True + r = self._aux(width) + if k_min >= 0: + # only positive wrap-arounds: r = expr - 2^w * k <= expr + self.constraints.append(f"(<= {r} {expr})") + elif k_max is not None and k_max <= 0: + # only borrows: no wrap iff expr >= 0 (no large constant needed) + self.constraints.append( + f"(or (and (<= 0 {expr}) (= {r} {expr})) (< {expr} 0))" + ) + return r + + r = self._aux(width) + if k_max is not None and k_max - k_min < MAX_WRAP_CASES: + cases = [] + for k in range(k_min, k_max + 1): + lo, hi = m * k, m * (k + 1) + shifted = expr if k == 0 else f"(- {expr} {lo})" + cases.append( + f"(and (<= {lo} {expr}) (< {expr} {hi}) (= {r} {shifted}))" + ) + self.constraints.append( + cases[0] if len(cases) == 1 else f"(or {' '.join(cases)})" + ) + else: + k = self._aux(None) + self.constraints.append(f"(= {r} (- {expr} (* {m} {k})))") + return r + + def _ite(self, cond: str, a: str, b: str, width: int | None) -> str: + """Arithmetic if-then-else, which PaSTTeL doesn't parse: a fresh t with + (cond and t = a) or (not cond and t = b).""" + t = self._aux(width) + self.constraints.append( + f"(or (and {cond} (= {t} {a})) (and (not {cond}) (= {t} {b})))" + ) + return t + + def _div_const(self, a: str, c: int) -> str: + """a div c for a constant c > 0: c*q <= a <= c*q + c - 1.""" + if c == 1: + return a + q = self._aux(None) + if self.small and c > MAX_SMALL_CONSTANT: + self.reg.approximate = True + # only keep 0 <= q <= a + self.constraints.append(f"(<= 0 {q})") + self.constraints.append(f"(<= {q} {a})") + return q + self.constraints.append(f"(<= (* {c} {q}) {a})") + self.constraints.append(f"(<= {a} (+ (* {c} {q}) {c - 1}))") + return q + + def _rem_const(self, a: str, c: int) -> str: + if self.small and c > MAX_SMALL_CONSTANT: + self.reg.approximate = True + r = self._aux(None) + self.constraints.append(f"(<= 0 {r})") + self.constraints.append(f"(<= {r} {a})") + return r + return f"(- {a} (* {c} {self._div_const(a, c)}))" + + def _truncate(self, a_term: str, a: BitVecRef, width: int) -> str: + """a (of a wider sort) mod 2^width.""" + upper = 2**width + no_wrap = ULT(a, BitVecVal(upper, a.size())) if upper < 2 ** a.size() else None + k_max = 2 ** (a.size() - width) - 1 + return self._wrap(a_term, width, 0, k_max, no_wrap) + + def _opaque(self, e: ExprRef) -> str: + """Opaque term: shared constant if loop-invariant, fresh value otherwise.""" + # a free input symbol is represented exactly by a variable; a compound + # term (keccak, storage read, bitwise operation...) is not + if e.num_args() > 0: + self.reg.approximate = True + width = e.size() if is_bv(e) else 1 + if not self.stem and not depends_on_loop_vars(e): + return self.reg.invariant(e) + if self.stem and e.get_id() in self.reg.invariants: + return self.reg.invariants[e.get_id()][0] + "_s" + return self._aux(width) + + @staticmethod + def _const(e: ExprRef) -> int | None: + return e.as_long() if is_bv_value(e) else None + + # entry points + + def bv(self, e: BitVecRef) -> str: + key = e.get_id() + if key not in self.memo: + self.memo[key] = self._bv(e) + return self.memo[key] + + def bool(self, e: BoolRef) -> str: + key = e.get_id() + if key not in self.memo: + self.memo[key] = self._bool(e) + return self.memo[key] + + def any(self, e: ExprRef) -> str: + """Int term for a bitvector, 0/1 for a boolean.""" + if is_bool(e): + return self._ite(self.bool(e), "1", "0", 1) + return self.bv(e) + + # bitvectors + + def _bv(self, e: BitVecRef) -> str: + w = e.size() + kind = e.decl().kind() + args = e.children() + + if kind == Z3_OP_BNUM: + return self._num(e.as_long()) + + if e.num_args() == 0 and kind == Z3_OP_UNINTERPRETED: + name = e.decl().name() + if name in self.names: + return self.names[name] + return self._opaque(e) + + if kind == Z3_OP_BADD: + # x + C with C >= 2^(w-1) is x - (2^w - C), e.g. solc's x - 1 + consts = [c for a in args if (c := self._const(a)) is not None] + neg = sum(2**w - c for c in consts if c >= 2 ** (w - 1)) + if neg and 0 < neg < 2**w and len(consts) < len(args): + rest = [a for a in args if not (self._const(a) or 0) >= 2 ** (w - 1)] + rest_expr = rest[0] if len(rest) == 1 else Sum(rest) + d = BitVecVal(neg, w) + return self._wrap( + f"(- {self.bv(rest_expr)} {self._num(neg)})", + w, + -1, + 0, + UGE(rest_expr, d), + ) + terms = " ".join(self.bv(a) for a in args) + wide = w + len(args).bit_length() + total = Sum([ZeroExt(wide - w, a) for a in args]) + no_wrap = ULT(total, BitVecVal(2**w, wide)) + return self._wrap(f"(+ {terms})", w, 0, len(args) - 1, no_wrap) + + if kind == Z3_OP_BSUB: + a, b = args + return self._wrap(f"(- {self.bv(a)} {self.bv(b)})", w, -1, 0, UGE(a, b)) + + if kind == Z3_OP_BNEG: + return self._wrap(f"(- 0 {self.bv(args[0])})", w, -1, 0, args[0] == 0) + + if kind == Z3_OP_BMUL: + consts = [c for a in args if (c := self._const(a)) is not None] + others = [a for a in args if self._const(a) is None] + if len(others) <= 1: + coef = 1 + for c in consts: + coef *= c + coef %= 2**w + if not others: + return self._num(coef) + return self._scale(others[0], coef, w) + return self._opaque(e) + + if kind == Z3_OP_BSHL and (s := self._const(args[1])) is not None: + if s >= w: + return "0" + return self._scale(args[0], 2**s, w) + + if kind == Z3_OP_BLSHR and (s := self._const(args[1])) is not None: + if s >= w: + return "0" + return self._div_const(self.bv(args[0]), 2**s) + + if kind in (Z3_OP_BUDIV, Z3_OP_BUDIV_I) and (c := self._const(args[1])): + return self._div_const(self.bv(args[0]), c) + + if kind in (Z3_OP_BUREM, Z3_OP_BUREM_I) and (c := self._const(args[1])): + return self._rem_const(self.bv(args[0]), c) + + if kind == Z3_OP_BAND: + # masking with 2^n - 1 is a truncation + consts = [c for a in args if (c := self._const(a)) is not None] + others = [a for a in args if self._const(a) is None] + if len(consts) == 1 and len(others) == 1: + mask = consts[0] + if mask & (mask + 1) == 0: + n = mask.bit_length() + if n >= w: + return self.bv(others[0]) + return self._truncate(self.bv(others[0]), others[0], n) + return self._opaque(e) + + if kind == Z3_OP_EXTRACT: + hi, lo = e.params() + a = args[0] + if lo == 0 and hi + 1 >= a.size(): + return self.bv(a) + if lo == 0: + return self._truncate(self.bv(a), a, hi + 1) + shifted = self._div_const(self.bv(a), 2**lo) + if hi + 1 >= a.size(): + return shifted + # (a >> lo) mod 2^(hi - lo + 1) + return self._truncate(shifted, LShR(a, lo), hi - lo + 1) + + if kind == Z3_OP_CONCAT: + acc = None + for a in args: + term = self.bv(a) + if acc is None: + acc = term + elif self.small and 2 ** a.size() > MAX_SMALL_CONSTANT: + # the high part is scaled by a large constant + zeros = all(self._const(x) == 0 for x in args[:-1]) + acc = term if zeros else self._opaque(e) + if not zeros: + return acc + else: + acc = f"(+ (* {2 ** a.size()} {acc}) {term})" + return acc + + if kind == Z3_OP_ZERO_EXT: + return self.bv(args[0]) + + if kind == Z3_OP_ITE: + return self._ite(self.bool(args[0]), self.bv(args[1]), self.bv(args[2]), w) + + # dolmos abstractions of nonlinear arithmetic: exact when the divisor is constant + if kind == Z3_OP_UNINTERPRETED and len(args) == 2: + name = e.decl().name() + c = self._const(args[1]) + if name.startswith("f_evm_bvudiv") and c: + return self._div_const(self.bv(args[0]), c) + if name.startswith("f_evm_bvurem") and c: + return self._rem_const(self.bv(args[0]), c) + if name.startswith("f_evm_bvmul"): + c0, c1 = self._const(args[0]), self._const(args[1]) + if c0 is not None or c1 is not None: + coef, other = (c0, args[1]) if c0 is not None else (c1, args[0]) + return self._scale(other, coef, w) + + return self._opaque(e) + + def _scale(self, x: BitVecRef, coef: int, w: int) -> str: + """(coef * x) mod 2^w for a constant coef.""" + if coef == 0: + return "0" + if coef == 1: + return self.bv(x) + if self.small and coef > MAX_SMALL_CONSTANT: + self.reg.approximate = True + r = self._aux(w) + return r + wide = w + coef.bit_length() + no_wrap = ULT(ZeroExt(wide - w, x) * coef, BitVecVal(2**w, wide)) + return self._wrap(f"(* {coef} {self.bv(x)})", w, 0, max(coef - 1, 0), no_wrap) + + # booleans + + def _signed(self, e: BitVecRef) -> str: + w = e.size() + a = self.bv(e) + return self._ite(f"(>= {a} {2 ** (w - 1)})", f"(- {a} {2**w})", a, None) + + def _signed_cmp(self, op: str, a: BitVecRef, b: BitVecRef) -> str: + if self.small: + # without large constants: exact only when both operands are + # provably non-negative, in which case it is the unsigned comparison + w = a.size() + half = BitVecVal(2 ** (w - 1), w) + if self.prover.valid(And(ULT(a, half), ULT(b, half))): + return f"({op} {self.bv(a)} {self.bv(b)})" + self.reg.approximate = True + aux = self._aux(1) + return f"(= {aux} 1)" + return f"({op} {self._signed(a)} {self._signed(b)})" + + def _bool(self, e: BoolRef) -> str: + kind = e.decl().kind() + args = e.children() + + if kind == Z3_OP_TRUE: + return "true" + if kind == Z3_OP_FALSE: + return "false" + if kind == Z3_OP_NOT: + return f"(not {self.bool(args[0])})" + if kind == Z3_OP_AND: + return f"(and {' '.join(self.bool(a) for a in args)})" + if kind == Z3_OP_OR: + return f"(or {' '.join(self.bool(a) for a in args)})" + if kind == Z3_OP_IMPLIES: + return f"(or (not {self.bool(args[0])}) {self.bool(args[1])})" + if kind in (Z3_OP_IFF, Z3_OP_EQ) and is_bool(args[0]): + a, b = self.bool(args[0]), self.bool(args[1]) + return f"(or (and {a} {b}) (and (not {a}) (not {b})))" + if kind == Z3_OP_XOR: + a, b = self.bool(args[0]), self.bool(args[1]) + return f"(or (and {a} (not {b})) (and (not {a}) {b}))" + if kind == Z3_OP_ITE: + c, a, b = (self.bool(x) for x in args) + return f"(or (and {c} {a}) (and (not {c}) {b}))" + + if kind == Z3_OP_EQ: + return f"(= {self.bv(args[0])} {self.bv(args[1])})" + if kind == Z3_OP_DISTINCT and len(args) == 2: + return f"(not (= {self.bv(args[0])} {self.bv(args[1])}))" + + unsigned = {Z3_OP_ULT: "<", Z3_OP_ULEQ: "<=", Z3_OP_UGT: ">", Z3_OP_UGEQ: ">="} + if kind in unsigned: + return f"({unsigned[kind]} {self.bv(args[0])} {self.bv(args[1])})" + + signed = {Z3_OP_SLT: "<", Z3_OP_SLEQ: "<=", Z3_OP_SGT: ">", Z3_OP_SGEQ: ">="} + if kind in signed: + return self._signed_cmp(signed[kind], args[0], args[1]) + + # opaque boolean: encoded as an Int in {0, 1} + self.reg.approximate = True + b = self._aux(1) + return f"(= {b} 1)" + + def conj(self, parts: list[str]) -> str: + parts = [p for p in parts + self.constraints if p != "true"] + if not parts: + return "true" + if len(parts) == 1: + return parts[0] + return f"(and {' '.join(parts)})" + + +# +# lasso construction +# + + +def slice_conditions(conditions: list[BoolRef], seeds: list[ExprRef]) -> list[BoolRef]: + """Keeps the conditions transitively sharing variables with the seeds.""" + cond_vars = [{v.get_id() for v in get_vars(c)} for c in conditions] + wanted = {v.get_id() for s in seeds for v in get_vars(s)} + kept: set[int] = set() + changed = True + while changed: + changed = False + for i, vs in enumerate(cond_vars): + if i not in kept and vs & wanted: + kept.add(i) + wanted |= vs + changed = True + return [c for i, c in enumerate(conditions) if i in kept] + + +@dataclass +class BodyPath: + conditions: list[BoolRef] # conditions added during the iteration + outputs: list[ExprRef] # new values of the loop variables (in order) + context: list[BoolRef] = field(default_factory=list) # all path conditions + + +_LARGE_LITERAL_RE = re.compile(r"(? None: + for m in _LARGE_LITERAL_RE.finditer(formula): + if int(m.group(0)) > MAX_SMALL_CONSTANT: + raise LassoError(f"internal error: large literal {m.group(0)} in the lasso") + + +def build_lasso( + loop_vars: list[str], + widths: list[int], + initial_values: list[ExprRef], + stem_conditions: list[BoolRef], + guard: BoolRef | None, + bodies: list[BodyPath], + exact_constants: bool = False, + stem_context: list[BoolRef] | None = None, + prover_timeout_ms: int = 1000, + complete: bool = False, +) -> dict: + """ + loop_vars[i]: name of the i-th fresh loop variable (lasso_v...); + the branching condition is the last loop variable (0/1 or raw word), and + `guard` (over the loop variables) must hold to stay in the loop. + """ + small = not exact_constants + reg = Registry() + prog = [f"v{i}" for i in range(len(loop_vars))] + in_names = {lv: f"{p}_i" for lv, p in zip(loop_vars, prog, strict=True)} + + def upper(name: str, w: int) -> list[str]: + if small and 2**w > MAX_SMALL_CONSTANT: + return [] + return [f"(< {name} {2**w})"] + + # facts for the wrap-around prover: the current iteration was entered by + # taking the loop branch at the end of a previous iteration (there is one, + # since the unrolling bound was reached). Only valid if the body paths are + # complete, as the previous iteration may have followed any of them. + previous: list[BoolRef] = [] + if small and complete and bodies: + cur = [BitVec(name, 256) for name in loop_vars] + prev = [BitVec(f"{name}_prev", 256) for name in loop_vars] + pairs = list(zip(cur, prev, strict=True)) + previous.append( + Or( + *[ + And( + *[substitute(c, *pairs) for c in b.conditions], + *[ + v == substitute(out, *pairs) + for v, out in zip(cur, b.outputs, strict=True) + ], + ) + for b in bodies + ] + ) + ) + + # loop transition: one disjunct per body path + disjuncts = [] + loop_aux: list[str] = [] + for body in bodies: + prover = ( + WrapProver([*body.context, *previous], prover_timeout_ms) if small else None + ) + tr = Translator(reg, in_names, stem=False, prover=prover) + parts = [tr.bool(guard)] if guard is not None else [] + parts += [tr.bool(c) for c in body.conditions] + for p, out in zip(prog, body.outputs, strict=True): + parts.append(f"(= {p}_o {tr.any(out)})") + disjuncts.append(tr.conj(parts)) + loop_aux += tr.aux + + ranges = [] + for p, w in zip(prog, widths, strict=True): + for suffix in ("_i", "_o"): + ranges += [f"(<= 0 {p}{suffix})", *upper(f"{p}{suffix}", w)] + for name, w, _ in reg.invariants.values(): + ranges += [f"(<= 0 {name})", *upper(name, w)] + + body_formula = ( + disjuncts[0] if len(disjuncts) == 1 else f"(or {' '.join(disjuncts)})" + ) + loop_formula = f"(and {' '.join(ranges)} {body_formula})" + + invariant_names = [name for name, _, _ in reg.invariants.values()] + + # stem: initial values of the loop variables and of the loop invariants + prover = ( + WrapProver(stem_context or stem_conditions, prover_timeout_ms) + if small + else None + ) + tr = Translator(reg, {}, stem=True, prover=prover) + parts = [tr.bool(c) for c in stem_conditions] + for p, init in zip(prog, initial_values, strict=True): + parts.append(f"(= {p}_s {tr.any(init)})") + for name, _, term in list(reg.invariants.values()): + parts.append(f"(= {name}_s {tr.any(term)})") + stem_ranges = [] + for p, w in zip(prog, widths, strict=True): + stem_ranges += [f"(<= 0 {p}_s)", *upper(f"{p}_s", w)] + for name, w, _ in reg.invariants.values(): + stem_ranges += [f"(<= 0 {name}_s)", *upper(f"{name}_s", w)] + stem_formula = tr.conj(stem_ranges + parts) + + # large literals are constant program variables, identical in the stem and + # the loop; their only known facts are positivity and their relative order + big_names = [reg.big_constants[v] for v in sorted(reg.big_constants)] + order = [f"(<= 0 {big_names[0]})"] if big_names else [] + order += [f"(< {a} {b})" for a, b in zip(big_names, big_names[1:], strict=False)] + if order: + loop_formula = f"(and {' '.join(order)} {loop_formula})" + stem_order = [re.sub(r"\b(c\d+)\b", r"\1_s", o) for o in order] + stem_formula = f"(and {' '.join(stem_order)} {stem_formula})" + + if small: + check_small_constants(loop_formula) + check_small_constants(stem_formula) + + all_vars = prog + invariant_names + big_names + constants = invariant_names + big_names + return reg.approximate, { + "program_vars": all_vars, + "var_types": {v: "Int" for v in all_vars}, + "stem": [ + { + "source": "entry", + "target": "head", + "formula": stem_formula, + "in_vars": {v: f"{v}_e" for v in all_vars}, + "out_vars": {v: f"{v}_s" for v in all_vars}, + "aux_vars": tr.aux, + "assigned_vars": all_vars, + } + ], + "loop": [ + { + "source": "head", + "target": "head", + "formula": loop_formula, + "in_vars": {**{p: f"{p}_i" for p in prog}, **{k: k for k in constants}}, + "out_vars": { + **{p: f"{p}_o" for p in prog}, + **{k: k for k in constants}, + }, + "aux_vars": loop_aux, + "assigned_vars": prog, + } + ], + } + + +def fresh_loop_var(index: int, uid: int) -> BitVecRef: + return BitVec(f"{LOOP_VAR_PREFIX}{uid}_{index}", 256) + + +def as_z3(x) -> ExprRef: + return x.as_z3() if hasattr(x, "as_z3") else x + + +def same_term(a, b) -> bool: + if a is b: + return True + za, zb = as_z3(a), as_z3(b) + if is_bool(za) != is_bool(zb): + return False + return za.eq(zb) + + +def branch_taken(cond: ExprRef, direction: bool) -> BoolRef: + """The condition for a JUMPI on `cond` to take the `direction` branch.""" + if is_bool(cond): + return cond if direction else simplify(Not(cond)) + return cond != 0 if direction else cond == 0 + + +# +# PaSTTeL +# + +_VERDICT_RE = re.compile(r"OVERALL RESULT:\s*(\S+)") +_PROOF_RE = re.compile( + r"^(\S+\([^)]*\)|\S+)\s+(TERMINATING|NON-TERMINATING)\s+[\d.]+\s+(.*)$", re.M +) + + +def run_pasttel(binary: str, lasso_path: str, timeout: int) -> tuple[str, str | None]: + try: + proc = subprocess.run( + [binary, "-a", "both", "-t", str(timeout), lasso_path], + capture_output=True, + text=True, + timeout=timeout + 10, + ) + except subprocess.TimeoutExpired: + return "TIMEOUT", None + except OSError as err: + return f"ERROR ({err})", None + + output = proc.stdout + proc.stderr + with open(lasso_path.removesuffix(".json") + ".pasttel.txt", "w") as f: + f.write(output) + + verdict = m.group(1) if (m := _VERDICT_RE.search(output)) else "UNKNOWN" + proof = None + for m in _PROOF_RE.finditer(output): + if m.group(2) == verdict: + proof = f"{m.group(1)}: {m.group(3).strip()}" + break + return verdict, proof + + +def write_lasso(out_dir: str, name: str, lasso: dict, meta: dict) -> str: + os.makedirs(out_dir, exist_ok=True) + path = os.path.join(out_dir, f"{name}.json") + with open(path, "w") as f: + json.dump(lasso, f, indent=2) + with open(os.path.join(out_dir, f"{name}.meta.json"), "w") as f: + json.dump(meta, f, indent=2, default=str) + return path diff --git a/src/dolmos/sevm.py b/src/dolmos/sevm.py index 3f97aad1..c1c8991a 100644 --- a/src/dolmos/sevm.py +++ b/src/dolmos/sevm.py @@ -54,7 +54,7 @@ ) from z3.z3util import is_expr_var -from dolmos import precompiles +from dolmos import lasso, precompiles from dolmos.bitvec import ONE, ZERO, is_power_of_two from dolmos.bitvec import DolmosBitVec as BV from dolmos.bitvec import DolmosBool as Bool @@ -371,6 +371,14 @@ def is_rlp_create_preimage(data: Bytes, size: int) -> bool: ) +def lasso_jumpid_tokens(ex) -> tuple: + """Jump destinations below the operands of the JUMPI about to be executed.""" + valid_jumpdests = ex.pgm.valid_jumpdests() + return tuple( + value for x in ex.st.stack[:-2] if (value := x.value) in valid_jumpdests + ) + + def jumpid_str(jumpid: JumpID) -> str: pc, jumpdests = jumpid return f"{pc}:{','.join(str(j) for j in jumpdests)}" @@ -1294,6 +1302,8 @@ def __init__(self, **kwargs) -> None: ) self.st = kwargs["st"] self.jumpis = kwargs["jumpis"] + # stack snapshots at symbolic JUMPIs, only recorded with --dump-lassos + self.loop_snapshots = kwargs.get("loop_snapshots") or {} self.addresses_to_delete = kwargs.get("addresses_to_delete") or set() # self.path = kwargs["path"] @@ -2184,9 +2194,11 @@ class DolmosLogs: def __init__(self) -> None: self.bounded_loops = [] + self.lassos: list[lasso.LassoResult] = [] def extend(self, logs: "DolmosLogs") -> None: self.bounded_loops.extend(logs.bounded_loops) + self.lassos.extend(logs.lassos) @dataclass(slots=True, eq=False, order=False) @@ -2220,6 +2232,13 @@ def __init__(self, options: DolmosConfig, fun_info: FunctionInfo) -> None: self.fun_info = fun_info self.logs = DolmosLogs() + # lasso extraction (--dump-lassos): the loop being probed, if any + self._probe = None + self._probe_steps = 0 + self._probe_flags: set[str] = set() + self._lasso_count = 0 + self._lassos_done: set = set() + # init storage model is_generic = self.options.storage_layout == "generic" self.storage_model = GenericStorage if is_generic else SolidityStorage @@ -2642,6 +2661,7 @@ def callback(new_ex: Exec, stack): new_ex.insn = ex.insn new_ex.st = deepcopy(ex.st) new_ex.jumpis = deepcopy(ex.jumpis) + new_ex.loop_snapshots = dict(ex.loop_snapshots) returndata = subcall.output.data copy_returndata_to_memory(returndata, ret_loc, ret_size, new_ex) @@ -3006,6 +3026,7 @@ def callback(new_ex: Exec, stack): new_ex.insn = ex.insn new_ex.st = deepcopy(ex.st) new_ex.jumpis = deepcopy(ex.jumpis) + new_ex.loop_snapshots = dict(ex.loop_snapshots) if subcall.is_stuck(): # internal errors abort the current path, @@ -3110,6 +3131,11 @@ def jumpi( jid = ex.jumpid() visited = ex.jumpis.get(jid, {True: 0, False: 0}) + dump_lassos = self.options.dump_lassos and self._probe is None + if is_symbolic_cond and dump_lassos: + prev_snapshot = ex.loop_snapshots.get(jid) + ex.loop_snapshots[jid] = lasso.LoopSnapshot(tuple(ex.st.stack), cond_true) + if is_symbolic_cond: # for loop unrolling follow_true = potential_true and visited[True] < self.options.loop @@ -3121,6 +3147,18 @@ def jumpi( if unroll_limit_reached_true or unroll_limit_reached_false: self.logs.bounded_loops.append(jid) + if dump_lassos: + for direction, reached in ( + (True, unroll_limit_reached_true), + (False, unroll_limit_reached_false), + ): + if reached and (jid, direction) not in self._lassos_done: + self._lassos_done.add((jid, direction)) + result = self.extract_lasso( + ex, jid, direction, target, cond_true, prev_snapshot + ) + self.logs.lassos.append(result) + # rendering ex.path to string can be expensive, so only do it if debug is enabled if self.options.debug: debug( @@ -3169,6 +3207,227 @@ def jumpi( } stack.push(new_ex_false) + def extract_lasso( + self, + ex: Exec, + jid: JumpID, + direction: bool, + target: int, + cond: BoolRef, + prev: "lasso.LoopSnapshot | None", + ) -> "lasso.LassoResult": + """ + Exports the loop at `jid` as a lasso program (see dolmos.lasso), and + optionally runs PaSTTeL on it. `ex` is at the JUMPI, operands popped. + """ + result = lasso.LassoResult( + test=self.fun_info.sig, jumpid=jumpid_str(jid), direction=direction + ) + try: + if prev is None: + raise lasso.LassoError("no snapshot of the previous iteration") + + stack_now = list(ex.st.stack) + if len(prev.stack) != len(stack_now): + raise lasso.LassoError("the stack height changes between iterations") + + carried = [ + i + for i in range(len(stack_now)) + if not lasso.same_term(prev.stack[i], stack_now[i]) + ] + + self._lasso_count += 1 + uid = self._lasso_count + + # extend the loop variables until the generalized iteration is closed + for _ in range(3): + bodies, extra, flags = self._generalized_iteration( + ex, jid, carried, direction, target, uid + ) + if not extra: + break + carried = sorted(set(carried) | extra) + else: + raise lasso.LassoError("the loop variables could not be determined") + + if not bodies: + raise lasso.LassoError( + "no path of the loop body returns to the loop head" + + (f" ({'; '.join(sorted(flags))})" if flags else "") + ) + + result.warnings = sorted(flags) + + # the cut point is right after the JUMPI: the stem ends by taking + # the loop branch, and each iteration ends by taking it again + loop_vars = [f"{lasso.LOOP_VAR_PREFIX}{uid}_{i}" for i in carried] + initial_values = [lasso.as_z3(stack_now[i]) for i in carried] + widths = [256] * len(loop_vars) + + stem_context = [*ex.path.conditions, lasso.branch_taken(cond, direction)] + stem_conditions = lasso.slice_conditions(stem_context, initial_values) + result.approximate, lasso_json = lasso.build_lasso( + loop_vars, + widths, + initial_values, + stem_conditions, + None, + bodies, + exact_constants=self.options.lasso_exact_constants, + stem_context=stem_context, + complete=not flags, + prover_timeout_ms=int(self.options.solver_timeout_branching * 1000), + ) + + name = re.sub( + r"[^A-Za-z0-9_]+", "_", f"{self.fun_info.name}_{jumpid_str(jid)}" + ) + name = f"{name}_{'t' if direction else 'f'}" + meta = { + "test": self.fun_info.sig, + "jumpid": jumpid_str(jid), + "loop_branch": direction, + "loop_bound": self.options.loop, + "loop_variables": { + f"v{k}": f"stack[{i}]" for k, i in enumerate(carried) + }, + "body_paths": len(bodies), + "encoding": ( + "exact" if self.options.lasso_exact_constants else "small-constants" + ), + "warnings": result.warnings, + "sound": not result.warnings, + # termination proofs are valid either way; non-termination + # witnesses are only meaningful for an exact lasso + "exact": not result.approximate and not result.warnings, + } + result.path = lasso.write_lasso( + self.options.dump_lassos, name, lasso_json, meta + ) + + if self.options.pasttel: + result.verdict, result.proof = lasso.run_pasttel( + self.options.pasttel, result.path, self.options.pasttel_timeout + ) + + except lasso.LassoError as err: + result.error = str(err) + except Exception as err: # the extraction must never break the test + result.error = f"{type(err).__name__}: {err}" + debug(f"lasso extraction failed: {err!r}") + finally: + self._probe = None + + return result + + def _generalized_iteration( + self, + ex: Exec, + jid: JumpID, + carried: list[int], + direction: bool, + target: int, + uid: int, + ) -> tuple[list["lasso.BodyPath"], set[int], set[str]]: + """ + Runs one iteration of the loop from a state where the loop-carried stack + slots and the branching condition are fresh symbols, on a separate solver. + Returns the body paths, the stack slots that turned out to change as + well, and warning flags. + """ + stack = list(ex.st.stack) + for i in carried: + stack[i] = BV(lasso.fresh_loop_var(i, uid)) + + timeout_ms = int(self.options.solver_timeout_branching * 1000) + path = Path(create_solver(timeout=timeout_ms)) + path.extend_path(ex.path) + stem_ids = {c.get_id() for c in path.conditions} + + gen_ex = Exec( + code=ex.code.copy(), + storage=deepcopy(ex.storage), + transient_storage=deepcopy(ex.transient_storage), + balance=ex.balance, + block=deepcopy(ex.block), + context=deepcopy(ex.context), + callback=None, # leaving the frame ends the iteration + pgm=ex.pgm, + pc=ex.pc, + st=State(stack=list(stack), memory=ex.st.memory.copy()), + jumpis={}, # nested loops get a fresh unrolling budget + path=path, + alias=ex.alias.copy(), + cnts=deepcopy(ex.cnts), + nonces=deepcopy(ex.nonces), + sha3s=ex.sha3s.copy(), + storages=ex.storages.copy(), + balances=ex.balances.copy(), + known_keys=ex.known_keys, + known_sigs=ex.known_sigs, + call_sequence=ex.call_sequence, + ) + if direction: + gen_ex.advance(pc=target + 1) + else: + gen_ex.advance() + + bodies: list[lasso.BodyPath] = [] + extra: set[int] = set() + # the loop head is identified like the unrolling counters (pc and jump + # destinations on the stack), plus the stack height (with the JUMPI + # operands), to tell apart the call sites of shared internal functions + self._probe = (ex.pc, ex.pgm, ex.context.depth, jid[1], len(stack) + 2) + self._probe_steps = 0 + self._probe_flags = set() + try: + for end_ex in self.run(gen_ex): + if not getattr(end_ex, "lasso_at_head", False): + if end_ex.context.output.data is None: + # stuck path, e.g. symbolic memory offset + self._probe_flags.add( + f"body path not explored: {end_ex.context.output.error}" + ) + continue # the path leaves the loop + + end_stack = end_ex.st.stack + if len(end_stack) != len(stack) + 2: + self._probe_flags.add( + f"stack height differs on a body path ({len(end_stack)} vs {len(stack)}+2)" + ) + continue + + # the iteration continues if the loop branch is taken again + again = lasso.branch_taken(lasso.as_z3(end_stack[-2]), direction) + if end_ex.check(again) == unsat: + continue # this path leaves the loop at the head + + below = end_stack[:-2] + outputs = [lasso.as_z3(below[i]) for i in carried] + extra |= { + j + for j in range(len(stack)) + if j not in carried and not lasso.same_term(below[j], stack[j]) + } + conditions = [ + c for c in end_ex.path.conditions if c.get_id() not in stem_ids + ] + conditions.append(again) + bodies.append( + lasso.BodyPath( + conditions, outputs, [*end_ex.path.conditions, again] + ) + ) + + if len(bodies) >= lasso.MAX_BODY_PATHS: + self._probe_flags.add("too many body paths") + break + finally: + self._probe = None + + return bodies, extra, set(self._probe_flags) + def create_branch(self, ex: Exec, cond: BitVecRef, target: int) -> Exec: new_path = ex.path.branch(cond) new_ex = Exec( @@ -3186,6 +3445,7 @@ def create_branch(self, ex: Exec, cond: BitVecRef, target: int) -> Exec: pc=target, st=deepcopy(ex.st), jumpis=deepcopy(ex.jumpis), + loop_snapshots=dict(ex.loop_snapshots), # path=new_path, alias=ex.alias.copy(), @@ -3348,6 +3608,9 @@ def finalize(ex: Exec): # not strictly necessary, but helps type checking ex: Exec | None = None + # set when this run is the generalized iteration of a loop (lasso extraction) + probe = self._probe + while (ex := next_ex or stack.pop()) is not None: try: next_ex = None @@ -3380,6 +3643,23 @@ def finalize(ex: Exec): if ex.context.depth > MAX_CALL_DEPTH: raise MessageDepthLimitError(ex.context) + # lasso extraction: stop when the probed loop comes back to its head + if probe is not None: + if ( + ex.pc == probe[0] + and ex.pgm is probe[1] + and ex.context.depth == probe[2] + and len(ex.st.stack) == probe[4] + and lasso_jumpid_tokens(ex) == probe[3] + ): + ex.lasso_at_head = True + yield ex + continue + self._probe_steps += 1 + if self._probe_steps > lasso.MAX_BODY_STEPS: + self._probe_flags.add("step budget exhausted") + return + insn: Instruction = ex.insn opcode: int = insn.opcode state: State = ex.st @@ -3594,6 +3874,8 @@ def finalize(ex: Exec): state.set_top(BV(value, size=256)) elif opcode == OP_SSTORE: + if probe is not None: + self._probe_flags.add("storage written in the loop body") slot: Word = state.popi() value: Word = state.popi() self.sstore(ex, ex.this(), slot, value) @@ -3853,6 +4135,10 @@ def finalize(ex: Exec): state.push_any(self.sload(ex, ex.this(), slot, transient=True)) elif opcode == OP_TSTORE: + if probe is not None: + self._probe_flags.add( + "transient storage written in the loop body" + ) slot: Word = state.popi() value: Word = state.popi() self.sstore(ex, ex.this(), slot, value, transient=True) diff --git a/tests/test_lasso.py b/tests/test_lasso.py new file mode 100644 index 00000000..fedc4136 --- /dev/null +++ b/tests/test_lasso.py @@ -0,0 +1,139 @@ +import json +import os +import shutil +import subprocess + +import pytest +from z3 import UGE, ULT, BitVec, BitVecVal, simplify + +from dolmos.lasso import ( + MAX_SMALL_CONSTANT, + BodyPath, + LassoError, + Registry, + Translator, + WrapProver, + build_lasso, + check_small_constants, +) + +M = 2**256 + + +def v(name: str): + return BitVec(f"lasso_v1_{name}", 256) + + +def small_translator(conditions, names=None): + prover = WrapProver(conditions, 1000) + return Translator(Registry(), names or {}, stem=False, prover=prover) + + +def test_add_without_wrap_is_exact(): + x = v("x") + tr = small_translator([ULT(x, 100)], {"lasso_v1_x": "x_i"}) + assert tr.bv(x + 1) == "(+ x_i 1)" + assert not tr.reg.approximate + + +def test_add_with_possible_wrap_is_approximated(): + x = v("x") + tr = small_translator([], {"lasso_v1_x": "x_i"}) + r = tr.bv(x + 1) + assert r.startswith("aux") + assert f"(<= {r} (+ x_i 1))" in tr.constraints + assert tr.reg.approximate + + +def test_minus_one_is_a_subtraction(): + # solc compiles x - 1 as x + (2^256 - 1) + x = v("x") + expr = simplify(x - 1) + tr = small_translator([UGE(x, 1)], {"lasso_v1_x": "x_i"}) + assert tr.bv(expr) == "(- x_i 1)" + + +def test_borrow_without_large_constant(): + x = v("x") + tr = small_translator([], {"lasso_v1_x": "x_i"}) + r = tr.bv(simplify(x - 2)) + assert any("(< (- x_i 2) 0)" in c for c in tr.constraints) + for c in tr.constraints: + check_small_constants(c) + assert r.startswith("aux") + + +def test_large_literals_become_constants(): + x = v("x") + tr = small_translator([], {"lasso_v1_x": "x_i"}) + term = tr.bv(x & BitVecVal(0xFF << 200, 256)) # not a low mask: opaque + tr2 = small_translator([], {"lasso_v1_x": "x_i"}) + lit = tr2.bool(ULT(x, BitVecVal(2**200, 256))) + assert lit == "(< x_i c0)" + assert term + assert tr2.reg.approximate + + +def test_exact_encoding_keeps_wrap_cases(): + x = v("x") + tr = Translator(Registry(), {"lasso_v1_x": "x_i"}, stem=False) + tr.bv(x + 1) + assert any(str(M) in c for c in tr.constraints) + + +def test_check_small_constants(): + check_small_constants(f"(< x {MAX_SMALL_CONSTANT})") + with pytest.raises(LassoError): + check_small_constants(f"(< x {M})") + + +def count_down_lasso(): + # while (x != 0) { x -= 1; } (checked arithmetic: x >= 1 on the path) + x = v("0") + out = simplify(x - 1) + again = out != 0 + body = BodyPath( + conditions=[UGE(x, 1), again], + outputs=[out], + context=[UGE(x, 1), again], + ) + n = BitVec("p_n_uint256", 256) + approximate, lasso = build_lasso( + ["lasso_v1_0"], + [256], + [n], + [n != 0], + None, + [body], + stem_context=[n != 0], + complete=True, + ) + return approximate, lasso + + +def test_count_down_lasso(): + approximate, lasso = count_down_lasso() + assert not approximate + loop = lasso["loop"][0] + assert "(= v0_o (- v0_i 1))" in loop["formula"] + check_small_constants(loop["formula"]) + check_small_constants(lasso["stem"][0]["formula"]) + assert lasso["program_vars"] == ["v0"] + + +@pytest.mark.skipif( + not (os.environ.get("PASTTEL") or shutil.which("pasttel")), + reason="PaSTTeL binary not available (set PASTTEL)", +) +def test_count_down_terminates_with_pasttel(tmp_path): + binary = os.environ.get("PASTTEL") or shutil.which("pasttel") + _, lasso = count_down_lasso() + path = tmp_path / "count_down.json" + path.write_text(json.dumps(lasso)) + out = subprocess.run( + [binary, "-a", "both", "-t", "30", str(path)], + capture_output=True, + text=True, + timeout=60, + ).stdout + assert "OVERALL RESULT: TERMINATING" in out From 1f9a136df15a24dfd96b1604db31e1bc1e60f4ee Mon Sep 17 00:00:00 2001 From: "Claude (draft)" Date: Fri, 25 Sep 2026 18:36:00 +0000 Subject: [PATCH 33/46] fix: storage slots written as precomputed keccak literals (halmos#579) The optimizer (notably via-IR) may constant-fold keccak256(k . p) into a PUSH32 literal, e.g. in a constructor writing m[k] = v, while runtime code computes the same slot with SHA3. A concrete slot is decoded as (p, k) only once its keccak preimage is known, so the first write was stored under the raw slot and the later read under (p, k): the value was lost and a false counterexample reported. When a structured access is resolved from a concrete slot, a value stored under the raw slot is now moved to the structured location first (solidity layout), or copied once (generic layout). StorageSlot.t.sol::check_keccak_slot_2_fail, which documented this limitation, now passes and is renamed accordingly. --- CHANGELOG.md | 4 + src/dolmos/sevm.py | 103 +++++++++++++++++++ tests/expected/all.json | 44 +++++++- tests/regression/test/PrecomputedSlots.t.sol | 60 +++++++++++ tests/regression/test/StorageSlot.t.sol | 6 +- 5 files changed, 211 insertions(+), 6 deletions(-) create mode 100644 tests/regression/test/PrecomputedSlots.t.sol diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ca6d707..37f6ce4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,10 @@ All notable changes to dolmos are documented here. The format follows Concrete inputs are now evaluated exactly; MODEXP follows EIP-198 (zero-padded input, output on `len(M)` bytes) and EIP-7823 (lengths above 1024 bytes make the call fail), and its symbolic model guarantees `result < M` (halmos#402) +- storage slots written as precomputed keccak256 literals (e.g. constant-folded by the + via-IR optimizer in a constructor) and read through SHA3 at runtime now hit the same + storage cell, instead of reading zero and reporting a false counterexample; applies to + both storage layouts (halmos#579) ## [0.1.0] - 2026-09-22 diff --git a/src/dolmos/sevm.py b/src/dolmos/sevm.py index c1c8991a..6bb6d6c3 100644 --- a/src/dolmos/sevm.py +++ b/src/dolmos/sevm.py @@ -1928,6 +1928,7 @@ def init( def load(cls, ex: Exec, storage: dict, addr: Any, loc: Word) -> Word: (slot, keys, num_keys, size_keys) = cls.get_key_structure(ex, loc) + cls.migrate_flat_slot(ex, storage, addr, loc, slot, keys) cls.init(ex, storage, addr, slot, keys, num_keys, size_keys) storage_addr = storage[addr] @@ -1950,6 +1951,7 @@ def load(cls, ex: Exec, storage: dict, addr: Any, loc: Word) -> Word: def store(cls, ex: Exec, storage: dict, addr: Any, loc: Any, val: Any) -> None: (slot, keys, num_keys, size_keys) = cls.get_key_structure(ex, loc) + cls.migrate_flat_slot(ex, storage, addr, loc, slot, keys) cls.init(ex, storage, addr, slot, keys, num_keys, size_keys) storage_addr = storage[addr] @@ -1969,6 +1971,51 @@ def store(cls, ex: Exec, storage: dict, addr: Any, loc: Any, val: Any) -> None: storage_addr[slot, num_keys, size_keys] = new_storage_var ex.storages[new_storage_var] = new_storage + @classmethod + def migrate_flat_slot( + cls, ex: Exec, storage: dict, addr: Any, loc: Any, slot: int, keys: tuple + ) -> None: + """ + Move a value stored under a raw concrete slot to its structured location. + + A concrete slot is decoded as `(slot, keys)` only if the keccak preimage of + the slot is known, i.e. once a SHA3 producing it has been executed (or it is + in the precomputed registry). The optimizer (notably via-IR) may constant-fold + `keccak256(k . p)` into a PUSH32 literal, e.g. in a constructor writing + `m[k] = v`, while the runtime code computes the same slot with SHA3. + The first access is then stored under the flat key `(0x,)` and the + later ones under `(p, k)`, so the value written by the former is lost + (a16z/halmos#579). When a structured access is resolved from a concrete + slot, the flat entry, if any, is moved to the structured location first. + """ + if not keys: + return + + loc = normalize(loc) + if not is_bv_value(loc): + return + + flat_key = (loc.as_long(), 0, 0) + storage_addr = storage[addr] + if flat_key not in storage_addr: + return + + val = storage_addr._mapping.pop(flat_key) + num_keys = len(keys) + size_keys = cls.bitsize(keys) + cls.init(ex, storage, addr, slot, keys, num_keys, size_keys) + + new_storage_var = Array( + f"storage_{id_str(addr)}_{slot}_{num_keys}_{size_keys}_{uid()}_{1 + len(ex.storages):>02}", + BitVecSorts[size_keys], + BitVecSort256, + ) + new_storage = Store(storage_addr[slot, num_keys, size_keys], concat(keys), val) + ex.path.append(new_storage_var == new_storage) + + storage_addr[slot, num_keys, size_keys] = new_storage_var + ex.storages[new_storage_var] = new_storage + @classmethod def get_key_structure(cls, ex, loc) -> tuple: offsets = cls.decode(ex, loc) @@ -2083,8 +2130,10 @@ def init( @classmethod def load(cls, ex: Exec, storage: dict, addr: Any, loc: Word) -> Word: + raw_loc = loc loc = cls.decode(ex, loc) size_keys = loc.size() + cls.migrate_flat_slot(ex, storage, addr, raw_loc, loc) cls.init(ex, storage, addr, loc, size_keys) @@ -2100,8 +2149,10 @@ def load(cls, ex: Exec, storage: dict, addr: Any, loc: Word) -> Word: @classmethod def store(cls, ex: Exec, storage: dict, addr: Any, loc: Any, val: Any) -> None: + raw_loc = loc loc = cls.decode(ex, loc) size_keys = loc.size() + cls.migrate_flat_slot(ex, storage, addr, raw_loc, loc) cls.init(ex, storage, addr, loc, size_keys) @@ -2118,6 +2169,58 @@ def store(cls, ex: Exec, storage: dict, addr: Any, loc: Any, val: Any) -> None: storage_addr[size_keys] = new_storage_var ex.storages[new_storage_var] = new_storage + @classmethod + def migrate_flat_slot( + cls, ex: Exec, storage: dict, addr: Any, raw_loc: Any, loc: BitVecRef + ) -> None: + """ + Same as SolidityStorage.migrate_flat_slot (a16z/halmos#579): a value written + under the raw 256-bit concrete slot, before its keccak preimage was known, + is copied to the decoded location. Each raw slot is migrated at most once, + so that later writes to the decoded location are not overwritten. + """ + raw_loc = normalize(raw_loc) + if not is_bv_value(raw_loc) or eq(raw_loc, loc): + return + + storage_addr = storage[addr] + flat_size = raw_loc.size() + if flat_size not in storage_addr: + return + + migrated = storage_addr.__dict__.setdefault("migrated_slots", set()) + raw_int = raw_loc.as_long() + if raw_int in migrated: + return + + # only migrate slots that were explicitly written under the raw key + arr = storage_addr[flat_size] + written = False + while arr in ex.storages: + store = ex.storages[arr] + if store.decl().name() != "store" or store.num_args() != 3: + break + if eq(store.arg(1), raw_loc): + written = True + break + arr = store.arg(0) + if not written: + return + + migrated.add(raw_int) + val = ex.select(storage_addr[flat_size], raw_loc, ex.storages) + size_keys = loc.size() + cls.init(ex, storage, addr, loc, size_keys) + new_storage_var = Array( + f"storage_{id_str(addr)}_{size_keys}_{uid()}_{1 + len(ex.storages):>02}", + BitVecSorts[size_keys], + BitVecSort256, + ) + new_storage = Store(storage_addr[size_keys], loc, val) + ex.path.append(new_storage_var == new_storage) + storage_addr[size_keys] = new_storage_var + ex.storages[new_storage_var] = new_storage + @classmethod def decode(cls, ex, loc: Any) -> Any: loc = normalize(loc) diff --git a/tests/expected/all.json b/tests/expected/all.json index 5791454d..e347f6b2 100644 --- a/tests/expected/all.json +++ b/tests/expected/all.json @@ -4148,6 +4148,44 @@ "num_bounded_loops": null } ], + "test/PrecomputedSlots.t.sol:PrecomputedSlotsTest": [ + { + "name": "check_mapping_literal_slot()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_nested_mapping_literal_slot()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_struct_field_literal_slot()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_write_after_literal_slot()", + "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)", @@ -6210,9 +6248,9 @@ "num_bounded_loops": null }, { - "name": "check_keccak_slot_2_fail(uint256)", - "exitcode": 1, - "num_models": 1, + "name": "check_keccak_slot_2_beyond_precomputed_pass(uint256)", + "exitcode": 0, + "num_models": 0, "models": null, "num_paths": null, "time": null, diff --git a/tests/regression/test/PrecomputedSlots.t.sol b/tests/regression/test/PrecomputedSlots.t.sol new file mode 100644 index 00000000..372c0f30 --- /dev/null +++ b/tests/regression/test/PrecomputedSlots.t.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +// a16z/halmos#579: the optimizer (notably via-IR) may constant-fold the keccak of a +// storage slot into a PUSH32 literal, e.g. in a constructor, while the runtime code +// computes the same slot with SHA3. Both accesses must hit the same storage cell. +// The literal slots below are written with inline assembly to emulate that folding. +contract PrecomputedSlots { + struct S { + uint256 a; + uint256 b; + } + + mapping(uint256 => uint256) public m; // slot 0 + mapping(uint256 => S) public ms; // slot 1 + mapping(uint256 => mapping(uint256 => uint256)) public mm; // slot 2 + + constructor() { + assembly { + // m[76] = 890: keccak256(abi.encode(76, 0)) + sstore(0xfec8fc345c07e1cb780845776a87475444a9ad5d006d1c0ba391a7299932fc3d, 890) + // ms[76].b = 7: keccak256(abi.encode(76, 1)) + 1 + sstore(0x1ed6ba569b422e6ce8625b852f3078402250527a73572e6a816cf9263aceeba2, 7) + // mm[76][5] = 42: keccak256(abi.encode(5, keccak256(abi.encode(76, 2)))) + sstore(0x9da36e986660358a9db129a989c5480cefb4582a99ba5113d8d083b4a63c69d7, 42) + } + } + + function incr() external { + m[76] += 1; + } +} + +contract PrecomputedSlotsTest { + PrecomputedSlots c; + + function setUp() public { + c = new PrecomputedSlots(); + } + + function check_mapping_literal_slot() public view { + assert(c.m(76) == 890); + } + + function check_struct_field_literal_slot() public view { + (uint256 a, uint256 b) = c.ms(76); + assert(a == 0); + assert(b == 7); + } + + function check_nested_mapping_literal_slot() public view { + assert(c.mm(76, 5) == 42); + } + + function check_write_after_literal_slot() public { + c.incr(); + c.incr(); + assert(c.m(76) == 892); + } +} diff --git a/tests/regression/test/StorageSlot.t.sol b/tests/regression/test/StorageSlot.t.sol index 07b0b7ea..7335bf2f 100644 --- a/tests/regression/test/StorageSlot.t.sol +++ b/tests/regression/test/StorageSlot.t.sol @@ -30,7 +30,6 @@ contract StorageSlotTest { } // this test passes because dolmos internally precomputes the slots for m[0] and m[1], for any m where slot(m) < 256. - // in general, however, directly initializing storage with a precomputed hash is not supported by dolmos. see the test below. function check_keccak_slot_2_pass(uint value) public { sstore(slot_map_one, value); // sstore with precomputed hash @@ -38,8 +37,9 @@ contract StorageSlotTest { assert(map[one] == value); // sload with keccak expression } - // this test failed due to m[2] beyond the scope of dolmos internal precomputation. see the above test for comparison. - function check_keccak_slot_2_fail(uint value) public { + // m[2] is beyond the scope of dolmos internal precomputation: the value written under the raw slot is + // moved to the decoded location once the keccak expression is evaluated (a16z/halmos#579). + function check_keccak_slot_2_beyond_precomputed_pass(uint value) public { sstore(slot_map_two, value); // sstore with precomputed hash assert(sload(slot_map_two) == value); // sload with precomputed hash From e64cf0396f212d3f98f6538b466e97a552f9a4d0 Mon Sep 17 00:00:00 2001 From: "Claude (draft)" Date: Fri, 25 Sep 2026 18:39:13 +0000 Subject: [PATCH 34/46] feat: support SELFDESTRUCT with EIP-6780 semantics (halmos#411) - the whole balance is sent to the beneficiary, and the frame halts successfully with empty return data; - WriteInStaticContext in a static context; - the account (code, storage, balance, nonce) is deleted at the end of the transaction only if it was created in that transaction; the deletions are propagated through CallOutput.accounts_to_delete on success and dropped on revert. Accounts created in the current transaction are tracked in Exec.created_in_tx, reset by SEVM.run(). UnsupportedOpcode.t.sol now relies on an undefined opcode (0x0c) instead of selfdestruct, and StaticContexts.t.sol::check_selfdestruct_fails is enabled. --- CHANGELOG.md | 4 + src/dolmos/sevm.py | 68 +++++++++ tests/expected/all.json | 103 ++++++++++++++ tests/regression/test/SelfDestruct.t.sol | 134 ++++++++++++++++++ tests/regression/test/StaticContexts.t.sol | 9 +- tests/regression/test/UnsupportedOpcode.t.sol | 6 +- 6 files changed, 318 insertions(+), 6 deletions(-) create mode 100644 tests/regression/test/SelfDestruct.t.sol diff --git a/CHANGELOG.md b/CHANGELOG.md index 37f6ce4a..2f1588a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,10 @@ All notable changes to dolmos are documented here. The format follows a small-constants encoding by default, because PaSTTeL computes with doubles and is unsound on 2^256 constants; `--lasso-exact-constants` gives the exact encoding. See docs/loop-termination.md +- `SELFDESTRUCT` opcode, with the EIP-6780 (Cancun) semantics: the balance is sent to + the beneficiary and the frame halts; the account (code, storage, balance, nonce) is + deleted at the end of the transaction only if it was created in that transaction; + forbidden in static contexts (halmos#411) ### Fixed diff --git a/src/dolmos/sevm.py b/src/dolmos/sevm.py index 6bb6d6c3..45e7b80b 100644 --- a/src/dolmos/sevm.py +++ b/src/dolmos/sevm.py @@ -137,6 +137,7 @@ OP_SAR, OP_SDIV, OP_SELFBALANCE, + OP_SELFDESTRUCT, OP_SGT, OP_SHA3, OP_SHL, @@ -1264,6 +1265,7 @@ class Exec: # an execution path st: State # stack and memory jumpis: dict[JumpID, dict[bool, int]] # for loop detection addresses_to_delete: set[Address] + created_in_tx: set[Address] # accounts created in the current transaction # path path: Path # path conditions @@ -1305,6 +1307,10 @@ def __init__(self, **kwargs) -> None: # stack snapshots at symbolic JUMPIs, only recorded with --dump-lassos self.loop_snapshots = kwargs.get("loop_snapshots") or {} self.addresses_to_delete = kwargs.get("addresses_to_delete") or set() + # accounts created in the current transaction (EIP-6780, SELFDESTRUCT). + # note: an empty set must be preserved as-is, so that sub-contexts share it + created_in_tx = kwargs.get("created_in_tx") + self.created_in_tx = created_in_tx if created_in_tx is not None else set() # self.path = kwargs["path"] self.alias = kwargs["alias"] @@ -2773,6 +2779,12 @@ def callback(new_ex: Exec, stack): subcall_success = subcall.output.error is None new_ex.st.push(ONE if subcall_success else ZERO) + if subcall_success: + # accounts self-destructed in the subcall are deleted at the end of the transaction + new_ex.context.output.accounts_to_delete |= ( + subcall.output.accounts_to_delete + ) + if not subcall_success: # revert network states new_ex.code = orig_code.copy() @@ -2813,6 +2825,7 @@ def callback(new_ex: Exec, stack): # cnts=ex.cnts, nonces=ex.nonces, + created_in_tx=ex.created_in_tx, sha3s=ex.sha3s, storages=ex.storages, balances=ex.balances, @@ -3107,6 +3120,9 @@ def create( # a newly created account starts with nonce 1 (EIP-161) ex.set_nonce(new_addr, 1) + # SELFDESTRUCT deletes the account only if it was created in the same transaction (EIP-6780) + ex.created_in_tx.add(new_addr) + # existing storage may not be empty and reset here ex.storage[new_addr] = self.mk_storagedata() ex.transient_storage[new_addr] = self.mk_storagedata() @@ -3140,6 +3156,11 @@ def callback(new_ex: Exec, stack): elif subcall.output.error is None: deployed_bytecode = subcall.output.data + # accounts self-destructed in the subcall are deleted at the end of the transaction + new_ex.context.output.accounts_to_delete |= ( + subcall.output.accounts_to_delete + ) + # new contract code, will revert if data is None new_code = Contract(deployed_bytecode) new_ex.try_resolve_contract_info(new_code, self.options.coverage_output) @@ -3192,6 +3213,7 @@ def callback(new_ex: Exec, stack): # cnts=ex.cnts, nonces=ex.nonces, + created_in_tx=ex.created_in_tx, sha3s=ex.sha3s, storages=ex.storages, balances=ex.balances, @@ -3464,6 +3486,7 @@ def _generalized_iteration( alias=ex.alias.copy(), cnts=deepcopy(ex.cnts), nonces=deepcopy(ex.nonces), + created_in_tx=set(ex.created_in_tx), sha3s=ex.sha3s.copy(), storages=ex.storages.copy(), balances=ex.balances.copy(), @@ -3555,6 +3578,7 @@ def create_branch(self, ex: Exec, cond: BitVecRef, target: int) -> Exec: # cnts=deepcopy(ex.cnts), nonces=deepcopy(ex.nonces), + created_in_tx=set(ex.created_in_tx), sha3s=ex.sha3s.copy(), storages=ex.storages.copy(), balances=ex.balances.copy(), @@ -3619,6 +3643,40 @@ 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 selfdestruct(self, ex: Exec) -> None: + """ + SELFDESTRUCT with the semantics introduced by EIP-6780 (Cancun): + - the whole balance of the executing account is sent to the beneficiary; + - the account is deleted, at the end of the transaction, only if it was + created in the same transaction. In that case, a balance sent to itself + is burnt. Otherwise, code, storage and nonce are left untouched. + - the current frame halts successfully with empty return data. + """ + beneficiary = uint160(ex.st.pop()) + + if ex.message().is_static: + raise WriteInStaticContext(ex.context_str()) + + this = ex.this() + balance = BV(ex.balance_of(this)) + self.transfer_value(ex, this, beneficiary.as_z3(), balance) + + if this in ex.created_in_tx: + ex.context.output.accounts_to_delete.add(this) + + ex.halt(data=ByteVec()) + + def delete_accounts(self, ex: Exec, addrs: set[Address]) -> None: + """Deletes the accounts self-destructed in the transaction that just ended.""" + for addr in addrs: + if addr in ex.code: + del ex.code[addr] + ex.storage[addr] = self.mk_storagedata() + ex.transient_storage[addr] = self.mk_storagedata() + ex.balance_update(addr, ZERO) + ex.set_nonce(addr, 0) + ex.created_in_tx.discard(addr) + def run_message(self, pre_ex: Exec, message: Message, path: Path) -> Iterator[Exec]: """ Executes the given transaction from the given input state. @@ -3654,6 +3712,9 @@ def run_message(self, pre_ex: Exec, message: Message, path: Path) -> Iterator[Ex yield from self.run(ex0) def run(self, ex0: Exec) -> Iterator[Exec]: + # each call to run() executes a new transaction + ex0.created_in_tx = set() + next_ex: Exec | None = ex0 stack: Worklist = Worklist() @@ -3675,6 +3736,8 @@ 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: + if output.error is None and output.accounts_to_delete: + self.delete_accounts(ex, output.accounts_to_delete) stack.completed_paths += 1 yield ex @@ -4296,6 +4359,11 @@ def finalize(ex: Exec): w2 = state.popi() state.push(w2.signextend(w1)) + elif opcode == OP_SELFDESTRUCT: + self.selfdestruct(ex) + yield from finalize(ex) + continue + else: # TODO: switch to InvalidOpcode when we have full opcode coverage # this halts the path, but we should only halt the current context diff --git a/tests/expected/all.json b/tests/expected/all.json index e347f6b2..c84d8082 100644 --- a/tests/expected/all.json +++ b/tests/expected/all.json @@ -4262,6 +4262,100 @@ "num_bounded_loops": null } ], + "test/SelfDestruct.t.sol:SelfDestructDeletedTest": [ + { + "name": "check_call_to_deleted_account()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_deleted_after_tx()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/SelfDestruct.t.sol:SelfDestructTest": [ + { + "name": "check_balance_moved_fail()", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_created_in_tx_alive_until_end_of_tx()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_destroy_in_constructor()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_halts_successfully()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_preexisting_keeps_code_and_storage()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_preexisting_self_beneficiary()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_reverted_selfdestruct()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_symbolic_beneficiary(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(uint256,bytes32,uint256)", @@ -5194,6 +5288,15 @@ "time": null, "num_bounded_loops": null }, + { + "name": "check_selfdestruct_fails()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, { "name": "check_sstore_fails()", "exitcode": 0, diff --git a/tests/regression/test/SelfDestruct.t.sol b/tests/regression/test/SelfDestruct.t.sol new file mode 100644 index 00000000..a2aaba55 --- /dev/null +++ b/tests/regression/test/SelfDestruct.t.sol @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import "forge-std/Test.sol"; + +// SELFDESTRUCT with the EIP-6780 semantics (a16z/halmos#411) + +contract Destructible { + uint256 public x = 1; + + constructor() payable {} + + function destroy(address payable beneficiary) external { + selfdestruct(beneficiary); + } + + function destroyThenRevert(address payable beneficiary) external { + this.destroy(beneficiary); + revert("undo"); + } + + receive() external payable {} +} + +contract DestroyInConstructor { + constructor(address payable beneficiary) payable { + selfdestruct(beneficiary); + } +} + +contract SelfDestructTest is Test { + Destructible preexisting; // created in setUp, i.e. in a previous transaction + address payable constant BENEFICIARY = payable(address(0xbeef)); + + function setUp() public { + preexisting = new Destructible{value: 1 ether}(); + } + + // not created in the current transaction: only the balance is transferred + function check_preexisting_keeps_code_and_storage() public { + uint256 before = BENEFICIARY.balance; + preexisting.destroy(BENEFICIARY); + + assertEq(address(preexisting).balance, 0); + assertEq(BENEFICIARY.balance, before + 1 ether); + assertGt(address(preexisting).code.length, 0); + assertEq(preexisting.x(), 1); + } + + // the frame halts: code after selfdestruct is not executed, and the call succeeds + function check_halts_successfully() public { + (bool success, bytes memory ret) = + address(preexisting).call(abi.encodeCall(Destructible.destroy, (BENEFICIARY))); + assertTrue(success); + assertEq(ret.length, 0); + } + + // sending the balance to itself is a no-op for a preexisting account + function check_preexisting_self_beneficiary() public { + preexisting.destroy(payable(address(preexisting))); + assertEq(address(preexisting).balance, 1 ether); + } + + // created in the current transaction: the account is deleted at the end of the transaction, + // so it is still alive until then + function check_created_in_tx_alive_until_end_of_tx() public { + Destructible d = new Destructible{value: 2 ether}(); + d.destroy(BENEFICIARY); + + assertEq(address(d).balance, 0); + assertEq(BENEFICIARY.balance, 2 ether); + assertGt(address(d).code.length, 0); + assertEq(d.x(), 1); + } + + // selfdestruct in the constructor: the value is forwarded, and the address still has code + // until the end of the transaction + function check_destroy_in_constructor() public { + address a = address(new DestroyInConstructor{value: 3 ether}(BENEFICIARY)); + assertEq(a.balance, 0); + assertEq(BENEFICIARY.balance, 3 ether); + } + + // a reverted selfdestruct has no effect + function check_reverted_selfdestruct() public { + try preexisting.destroyThenRevert(BENEFICIARY) { + assert(false); + } catch {} + assertEq(address(preexisting).balance, 1 ether); + assertEq(BENEFICIARY.balance, 0); + } + + // symbolic beneficiary + function check_symbolic_beneficiary(address payable to) public { + vm.assume(to != address(preexisting)); + uint256 before = to.balance; + vm.assume(before <= 100 ether); + preexisting.destroy(to); + assertEq(to.balance, before + 1 ether); + } + + // expected failure: sanity check that assertions on the transfer are meaningful + function check_balance_moved_fail() public { + preexisting.destroy(BENEFICIARY); + assertEq(address(preexisting).balance, 1 ether); + } +} + +// accounts created and self-destructed in the same transaction (here, setUp) are deleted +// at the end of that transaction (EIP-6780) +contract SelfDestructDeletedTest is Test { + Destructible d; + Destructible kept; + address payable constant BENEFICIARY = payable(address(0xbeef)); + + function setUp() public { + d = new Destructible{value: 1 ether}(); + d.destroy(BENEFICIARY); + kept = new Destructible(); + } + + function check_deleted_after_tx() public view { + assertEq(address(d).code.length, 0); + assertEq(address(d).balance, 0); + assertEq(BENEFICIARY.balance, 1 ether); + assertGt(address(kept).code.length, 0); + } + + function check_call_to_deleted_account() public { + (bool success, bytes memory ret) = address(d).call(abi.encodeWithSignature("x()")); + assertTrue(success); // call to an account without code succeeds + assertEq(ret.length, 0); + } +} diff --git a/tests/regression/test/StaticContexts.t.sol b/tests/regression/test/StaticContexts.t.sol index d63f3da1..11fdab58 100644 --- a/tests/regression/test/StaticContexts.t.sol +++ b/tests/regression/test/StaticContexts.t.sol @@ -64,9 +64,8 @@ contract StaticContextsTest is Test { // assertFalse(success); // } - // TODO: selfdestruct not implemented yet - // function check_selfdestruct_fails() public { - // (bool success, ) = address(this).staticcall(abi.encodeWithSignature("do_selfdestruct()")); - // assertFalse(success); - // } + function check_selfdestruct_fails() public { + (bool success, ) = address(this).staticcall(abi.encodeWithSignature("do_selfdestruct()")); + assertFalse(success); + } } diff --git a/tests/regression/test/UnsupportedOpcode.t.sol b/tests/regression/test/UnsupportedOpcode.t.sol index b3981571..a320c397 100644 --- a/tests/regression/test/UnsupportedOpcode.t.sol +++ b/tests/regression/test/UnsupportedOpcode.t.sol @@ -5,7 +5,11 @@ pragma solidity >=0.8.0 <0.9.0; contract X { function foo() internal { assembly { - selfdestruct(0) // unsupported opcode + // deploy a contract whose runtime code is the undefined opcode 0x0c, and call it + // initcode: PUSH1 0x0c PUSH0 MSTORE8 PUSH1 0x01 PUSH0 RETURN + mstore(0, shl(192, 0x600c5f5360015ff3)) + let target := create(0, 0, 8) + pop(call(gas(), target, 0, 0, 0, 0, 0)) // unsupported opcode } } From c186c30553506bc89ad0190c6b197d59f2a6b50e Mon Sep 17 00:00:00 2001 From: "Claude (draft)" Date: Fri, 25 Sep 2026 19:12:35 +0000 Subject: [PATCH 35/46] fix: check counterexamples against real keccak256 values (halmos#562) keccak256 is modeled as uninterpreted functions, so a model may assign to a symbolic preimage a hash that differs from the real keccak256, yielding counterexamples that do not replay (e.g. amt = 0 hashing to a storage key written in setUp). When the query is sat, the preimages of the symbolic keccak terms are read from the model (through definitional sha3arg_* constants, neutral for unsat cores), and the facts f_sha3_N(v) == keccak256(v) are added before solving again. These facts hold for the real hash function, so no real counterexample is lost, and unsat proves that none exists under the usual keccak assumptions. After --keccak-refinement-rounds rounds (default 3), a model that still disagrees is reported as potentially invalid. Unsat cores of refined queries are not cached, as they may depend on the unnamed facts. --- CHANGELOG.md | 6 + src/dolmos/__main__.py | 2 + src/dolmos/config.py | 9 + src/dolmos/solve.py | 165 ++++++++++++++++++- tests/expected/all.json | 29 ++++ tests/regression/test/KeccakRefinement.t.sol | 37 +++++ tests/test_solve.py | 47 +++++- 7 files changed, 291 insertions(+), 4 deletions(-) create mode 100644 tests/regression/test/KeccakRefinement.t.sol diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f1588a8..2fd9c640 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,12 @@ All notable changes to dolmos are documented here. The format follows via-IR optimizer in a constructor) and read through SHA3 at runtime now hit the same storage cell, instead of reading zero and reporting a false counterexample; applies to both storage layouts (halmos#579) +- counterexamples relying on keccak256 values that differ from the real hash function + (keccak256 is uninterpreted, so the solver could pick e.g. `x = 0` and assume that its + hash is the key of a storage entry) are no longer reported as valid. The real hashes of + the preimages chosen by the model are added to the query, which is solved again, up to + `--keccak-refinement-rounds` times (default 3, 0 disables the check); a counterexample + that still disagrees is reported as potentially invalid (halmos#562) ## [0.1.0] - 2026-09-22 diff --git a/src/dolmos/__main__.py b/src/dolmos/__main__.py index e62ea76f..6f76dc04 100644 --- a/src/dolmos/__main__.py +++ b/src/dolmos/__main__.py @@ -117,6 +117,7 @@ PathContext, SolverOutput, dirname, + mk_sha3_args, solve_end_to_end, solve_low_level, ) @@ -952,6 +953,7 @@ def handle_assertion_violation( path_id=path_id, query=query, solving_ctx=ctx.solving_ctx, + sha3_args=mk_sha3_args(ex.sha3s) if args.keccak_refinement_rounds else (), ) # ShutdownError may be raised here and will be handled by the caller diff --git a/src/dolmos/config.py b/src/dolmos/config.py index bb88ea8e..69432209 100644 --- a/src/dolmos/config.py +++ b/src/dolmos/config.py @@ -614,6 +614,15 @@ class Config: group=solver_group, ) + keccak_refinement_rounds: int = arg( + help="when a counterexample relies on keccak values inconsistent with the real hash function, " + "add the real hash values to the query and solve again, at most this many times; " + "a counterexample that is still inconsistent is reported as potentially invalid (0 disables the check)", + global_default=3, + metavar="N", + group=solver_group, + ) + ### Experimental options symbolic_jump: bool = arg( diff --git a/src/dolmos/solve.py b/src/dolmos/solve.py index d38edfcd..eee4fc41 100644 --- a/src/dolmos/solve.py +++ b/src/dolmos/solve.py @@ -4,12 +4,13 @@ import shlex import subprocess from concurrent.futures import ThreadPoolExecutor -from dataclasses import dataclass, field +from dataclasses import dataclass, field, replace from pathlib import Path from tempfile import TemporaryDirectory from types import MappingProxyType from typing import Literal +from eth_hash.auto import keccak from z3 import CheckSatResult, Solver, sat, unknown, unsat from dolmos.calldata import FunctionInfo @@ -25,7 +26,7 @@ PopenFuture, ) from dolmos.sevm import Address, Exec, SMTQuery -from dolmos.utils import hexify +from dolmos.utils import f_sha3_name, hexify, is_bv_value, is_f_sha3_name EXIT_TIMEDOUT = 124 @@ -284,10 +285,16 @@ class PathContext: query: SMTQuery is_refined: bool = False + # symbolic keccak preimages of the path, as (bitsize, smtlib term); see mk_sha3_args() + sha3_args: tuple[tuple[int, str], ...] = () + + # distinguishes the query files of keccak refinement rounds + suffix: str = "" + @property def dump_file(self) -> Path: refined_str = ".refined" if self.is_refined else "" - filename = f"{self.path_id}{refined_str}.smt2" + filename = f"{self.path_id}{refined_str}{self.suffix}.smt2" return Path(dirname(self.solving_ctx.dump_dir)) / filename @@ -298,6 +305,8 @@ def refine(self) -> "PathContext": solving_ctx=self.solving_ctx, query=refine(self.query), is_refined=True, + sha3_args=self.sha3_args, + suffix=self.suffix, ) @@ -547,6 +556,22 @@ def solve_low_level(path_ctx: PathContext) -> SolverOutput: def solve_end_to_end(ctx: PathContext) -> SolverOutput: + """Solves the given query, then checks the keccak values the model relies on (see refine_keccak).""" + if not ctx.sha3_args or ctx.args.keccak_refinement_rounds <= 0: + return _solve_end_to_end(ctx) + + # expose the keccak preimages in the model with definitional (hence unsat-core neutral) constraints + ctx = replace( + ctx, + query=SMTQuery( + ctx.query.smtlib + sha3_arg_decls(ctx.sha3_args), ctx.query.assertions + ), + ) + solver_output = _solve_end_to_end(ctx) + return refine_keccak(ctx, solver_output) + + +def _solve_end_to_end(ctx: PathContext) -> SolverOutput: """Synchronously resolves a query in a given context, which may result in 0, 1 or multiple solver invocations. - may result in 0 invocations if the query contains a known unsat core (hence the need for the context) @@ -585,6 +610,140 @@ def solve_end_to_end(ctx: PathContext) -> SolverOutput: return solver_output +# keccak refinement (a16z/halmos#562) +# +# keccak256 is modeled as uninterpreted functions f_sha3_N, constrained only by the +# injectivity and range axioms, and by the concrete values of hashes of concrete data. +# A model may thus assign to a symbolic preimage x a hash value f_sha3_N(x) that differs +# from the real keccak256(x), e.g. the solver may pick x = 0 and "decide" that its hash +# is the key of a storage entry written in setUp. Such a counterexample does not replay. +# +# When the query is sat, the value of every symbolic preimage is read from the model, and +# the real hashes of these values are asserted, `f_sha3_N(v) == keccak256(v)`. These facts +# are true of the real hash function, so the refined query keeps every real counterexample: +# - if the model agrees with the real hashes, the counterexample is kept as is; +# - otherwise the query is solved again with the new facts: unsat means that no counterexample +# exists under the real hash values (modulo the usual keccak assumptions), and a new model is +# checked again, up to --keccak-refinement-rounds times; a model that still disagrees is +# reported as potentially invalid. + +SHA3_ARG_PREFIX = "sha3arg_" + +sha3_arg_pattern = re.compile( + rf"\(\s*define-fun\s+\|?({SHA3_ARG_PREFIX}\d+)\|?\s+\(\)\s+\(_\s+BitVec\s+(\d+)\)\s+" + r"(#x[0-9a-fA-F]+|#b[01]+|\(_\s+bv\d+\s+\d+\))\s*\)" +) + +MAX_SHA3_ARGS = 64 + + +def mk_sha3_args(sha3s) -> tuple[tuple[int, str], ...]: + """ + Returns the symbolic keccak preimages registered in the given KeccakRegistry, as + (bitsize, smtlib term) pairs. Strings only: the result crosses thread boundaries. + """ + result = [] + for expr in sha3s: + if expr.num_args() != 1 or not is_f_sha3_name(expr.decl().name()): + continue + data = expr.arg(0) + if is_bv_value(data): + continue + result.append((data.size(), data.sexpr())) + if len(result) >= MAX_SHA3_ARGS: + break + return tuple(result) + + +def sha3_arg_decls(sha3_args) -> str: + return "".join( + f"(declare-fun {SHA3_ARG_PREFIX}{i} () (_ BitVec {size}))\n" + f"(assert (= {SHA3_ARG_PREFIX}{i} {term}))\n" + for i, (size, term) in enumerate(sha3_args) + ) + + +def parse_sha3_arg_values(solver_stdout: str) -> dict[int, tuple[int, int]]: + """Returns {index: (bitsize, value)} for the sha3arg_* constants of the model""" + values = {} + for match in sha3_arg_pattern.finditer(solver_stdout): + index = int(match.group(1)[len(SHA3_ARG_PREFIX) :]) + values[index] = (int(match.group(2)), parse_const_value(match.group(3))) + return values + + +def keccak_fact(size: int, value: int) -> tuple[str, int] | None: + """Returns the assertion `f_sha3_(value) == keccak256(value)`, and the hash""" + hash_value = int.from_bytes(keccak(value.to_bytes(size // 8, "big")), "big") + + # outside of the range assumed for hash values; see Exec.sha3_data() + if hash_value == 0 or hash_value > 2**256 - 2**64: + return None + + fact = f"(assert (= ({f_sha3_name(size)} (_ bv{value} {size})) (_ bv{hash_value} 256)))\n" + return fact, hash_value + + +def refine_keccak(ctx: PathContext, solver_output: SolverOutput) -> SolverOutput: + verbose = print if ctx.args.verbose >= 1 else lambda *args, **kwargs: None + + facts: dict[tuple[int, int], str] = {} + base_smtlib = ctx.query.smtlib + max_rounds = ctx.args.keccak_refinement_rounds + + for round in range(max_rounds + 1): + result, model = solver_output.result, solver_output.model + if result != sat or model is None or not model.is_valid: + return solver_output + + try: + with open(f"{solver_output.query_file}.out") as f: + values = parse_sha3_arg_values(f.read()) + except OSError: + return solver_output + + new_facts = {} + consistent = True + for size, value in values.values(): + if size % 8 or (size, value) in facts: + continue + fact = keccak_fact(size, value) + if fact is None: + consistent = False + continue + new_facts[size, value] = fact[0] + + if not new_facts: + if consistent: + return solver_output + break + + if round == max_rounds: + break + + facts.update(new_facts) + verbose( + f" Checking again with {len(facts)} keccak value(s) (round {round + 1})" + ) + + refined_ctx = replace( + ctx, + query=SMTQuery(base_smtlib + "".join(facts.values()), ctx.query.assertions), + suffix=f".keccak{round + 1}", + ) + solver_output = solve_low_level(refined_ctx) + + # an unsat core of the refined query may depend on the (unnamed) keccak facts, + # so it cannot be reused for other queries + if solver_output.unsat_core is not None: + solver_output = replace(solver_output, unsat_core=None) + + # the model still relies on keccak values that may not exist + verbose(" Keccak values of the model are not consistent with keccak256") + potential_model = PotentialModel(model=solver_output.model.model, is_valid=False) + return replace(solver_output, model=potential_model) + + def check_unsat_cores(query: SMTQuery, unsat_cores: list[list]) -> bool: # return true if the given query contains any given unsat core for unsat_core in unsat_cores: diff --git a/tests/expected/all.json b/tests/expected/all.json index c84d8082..6e526ec4 100644 --- a/tests/expected/all.json +++ b/tests/expected/all.json @@ -2875,6 +2875,35 @@ "num_bounded_loops": null } ], + "test/KeccakRefinement.t.sol:KeccakRefinementTest": [ + { + "name": "check_keccak_injective(uint256,uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_keccak_known_preimage(uint256)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_keccak_unknown_preimage(uint256)", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], "test/Library.t.sol:LibraryTest": [ { "name": "check_add(uint256,uint256)", diff --git a/tests/regression/test/KeccakRefinement.t.sol b/tests/regression/test/KeccakRefinement.t.sol new file mode 100644 index 00000000..1c4631f1 --- /dev/null +++ b/tests/regression/test/KeccakRefinement.t.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +// a16z/halmos#562: keccak256 is uninterpreted, so the solver may assign to a symbolic +// preimage a hash value that differs from the real one. Such counterexamples are checked +// against the real keccak256 values, and reported as potentially invalid if they do not agree. +contract KeccakRefinementTest { + mapping(uint256 => uint256) balances; + + // not a constant, to prevent the optimizer from folding the slot computation + uint256 key = 0xfaaaaaffafafafafaaaaa472134; + + function setUp() public { + balances[key] = 50; + } + + // the only counterexamples need a preimage of `key`, which is not known: + // the counterexample must not be reported as valid + function check_keccak_unknown_preimage(uint256 amt) public view { + bytes32 hash = keccak256(abi.encodePacked(amt)); + uint256 balance = balances[uint256(hash)]; + assert(balance != 50); + } + + // a genuine counterexample involving keccak256 is still reported as valid: amt == 42 + function check_keccak_known_preimage(uint256 amt) public pure { + bytes32 hash = keccak256(abi.encodePacked(amt)); + assert(hash != keccak256(abi.encodePacked(uint256(42)))); + } + + // properties that hold are not affected + function check_keccak_injective(uint256 a, uint256 b) public pure { + if (a != b) { + assert(keccak256(abi.encodePacked(a)) != keccak256(abi.encodePacked(b))); + } + } +} diff --git a/tests/test_solve.py b/tests/test_solve.py index ecd419a5..f40c67f3 100644 --- a/tests/test_solve.py +++ b/tests/test_solve.py @@ -1,6 +1,13 @@ import pytest +from eth_hash.auto import keccak -from dolmos.solve import ModelVariable, parse_model_str +from dolmos.solve import ( + ModelVariable, + keccak_fact, + parse_model_str, + parse_sha3_arg_values, + sha3_arg_decls, +) @pytest.mark.parametrize( @@ -110,3 +117,41 @@ def test_smtlib_stp_output(full_name): size_bits=8, value=4, ) + + +@pytest.mark.parametrize( + "smtlib_str", + [ + # z3 + "(define-fun sha3arg_0 () (_ BitVec 256)\n #x000000000000000000000000000000000000000000000000000000000000002a)" + "(define-fun sha3arg_1 () (_ BitVec 16) #x0001)", + # yices --smt2-model-format --bvconst-in-decimal + "(define-fun sha3arg_0 () (_ BitVec 256) (_ bv42 256))" + "(define-fun sha3arg_1 () (_ BitVec 16) (_ bv1 16))", + # yices --smt2-model-format + "(define-fun sha3arg_0 () (_ BitVec 256) #b" + "0" * 250 + "101010)" + "(define-fun sha3arg_1 () (_ BitVec 16) #b0000000000000001)", + ], +) +def test_parse_sha3_arg_values(smtlib_str): + values = parse_sha3_arg_values( + "sat\n(define-fun p_x_uint256_00 () (_ BitVec 256) (_ bv7 256))\n" + smtlib_str + ) + assert values == {0: (256, 42), 1: (16, 1)} + + +def test_keccak_fact(): + fact, hash_value = keccak_fact(256, 42) + expected = int.from_bytes(keccak((42).to_bytes(32, "big")), "big") + assert hash_value == expected + assert fact == f"(assert (= (f_sha3_256 (_ bv42 256)) (_ bv{expected} 256)))\n" + + +def test_sha3_arg_decls(): + decls = sha3_arg_decls(((256, "p_x_uint256_00"), (512, "(concat a b)"))) + assert decls == ( + "(declare-fun sha3arg_0 () (_ BitVec 256))\n" + "(assert (= sha3arg_0 p_x_uint256_00))\n" + "(declare-fun sha3arg_1 () (_ BitVec 512))\n" + "(assert (= sha3arg_1 (concat a b)))\n" + ) From 9bd6829d0994f50380b5a41ea6b2e287f630fc2e Mon Sep 17 00:00:00 2001 From: "Claude (draft)" Date: Fri, 25 Sep 2026 19:29:26 +0000 Subject: [PATCH 36/46] feat: EVM version setting, --evm-version (halmos#128) The EVM version (hard fork) determines: - the available opcodes: an opcode introduced after the selected fork halts the current frame with InvalidOpcode, as on a node running that fork (DELEGATECALL, REVERT/RETURNDATA*/STATICCALL, shifts/CREATE2/ EXTCODEHASH, CHAINID/SELFBALANCE, BASEFEE, PUSH0, TLOAD/TSTORE/MCOPY/ BLOBHASH/BLOBBASEFEE, CLZ); - the semantics of SELFDESTRUCT: before Cancun, the account is deleted at the end of the transaction even if it was not created in it (EIP-6780 from Cancun). The option can be given on the command line, in dolmos.toml, or in @custom:dolmos annotations. When not given, the evmVersion found in the compiler metadata of the test contract (foundry's evm_version) is used, or the latest supported fork (osaka). With the latest fork, the opcode check is skipped entirely, so the default behavior and performance are unchanged. Not modeled yet: fork-dependent precompiles and gas rules. --- CHANGELOG.md | 5 + src/dolmos/__main__.py | 36 +++++++ src/dolmos/config.py | 8 ++ src/dolmos/evm_version.py | 124 ++++++++++++++++++++++ src/dolmos/sevm.py | 38 ++++++- tests/expected/all.json | 64 ++++++++++++ tests/regression/test/EvmVersion.t.sol | 62 +++++++++++ tests/regression/test/SelfDestruct.t.sol | 38 +++++++ tests/test_evm_version.py | 125 +++++++++++++++++++++++ 9 files changed, 498 insertions(+), 2 deletions(-) create mode 100644 src/dolmos/evm_version.py create mode 100644 tests/regression/test/EvmVersion.t.sol create mode 100644 tests/test_evm_version.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fd9c640..6ddeb363 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,11 @@ All notable changes to dolmos are documented here. The format follows the beneficiary and the frame halts; the account (code, storage, balance, nonce) is deleted at the end of the transaction only if it was created in that transaction; forbidden in static contexts (halmos#411) +- EVM version setting, `--evm-version VERSION` (also usable in `dolmos.toml` and + `@custom:dolmos` annotations): opcodes introduced after the selected fork are invalid, + and `SELFDESTRUCT` deletes the account unconditionally before Cancun. Defaults to the + `evmVersion` the test contract was compiled for (foundry's `evm_version`), or to the + latest supported fork, `osaka` (halmos#128) ### Fixed diff --git a/src/dolmos/__main__.py b/src/dolmos/__main__.py index 6f76dc04..6a36a776 100644 --- a/src/dolmos/__main__.py +++ b/src/dolmos/__main__.py @@ -69,6 +69,12 @@ ) from dolmos.contract import CoverageReporter from dolmos.env import init_env +from dolmos.evm_version import ( + EVM_VERSIONS, + LATEST_EVM_VERSION, + evm_version_from_metadata, + normalize_evm_version, +) from dolmos.exceptions import ( DolmosException, FailCheatcode, @@ -203,6 +209,35 @@ def with_devdoc(args: DolmosConfig, fn_sig: str, contract_json: dict) -> DolmosC return args.with_overrides(source, **vars(overrides)) +def with_evm_version(args: DolmosConfig, contract_json: dict) -> DolmosConfig: + """ + Resolves the EVM version (a16z/halmos#128): the --evm-version option if given, + otherwise the evmVersion the contract was compiled for, otherwise the latest fork. + """ + if args.evm_version: + if normalize_evm_version(args.evm_version) is None: + error( + f"unsupported EVM version: {args.evm_version} " + f"(supported: {', '.join(EVM_VERSIONS)})" + ) + sys.exit(2) + return args + + compiled_for = evm_version_from_metadata(contract_json) + if compiled_for is None: + return args + + version = normalize_evm_version(compiled_for) + if version is None: + warn( + f"unsupported evmVersion {compiled_for} in the build output, " + f"using {LATEST_EVM_VERSION} (see --evm-version)" + ) + return args + + return args.with_overrides(ConfigSource.default, evm_version=version) + + def with_natspec( args: DolmosConfig, contract_name: str, contract_natspec: str ) -> DolmosConfig: @@ -1991,6 +2026,7 @@ def on_signal(signum, frame): # support for `/// @custom:dolmos` annotations contract_args = with_natspec(args, contract_name, natspec) + contract_args = with_evm_version(contract_args, contract_json) contract_ctx = ContractContext( args=contract_args, name=contract_name, diff --git a/src/dolmos/config.py b/src/dolmos/config.py index 69432209..91812da9 100644 --- a/src/dolmos/config.py +++ b/src/dolmos/config.py @@ -378,6 +378,14 @@ class Config: choices=["solidity", "generic"], ) + evm_version: str = arg( + help="EVM version (hard fork) to execute against, e.g. shanghai, cancun, prague, osaka; " + "determines the available opcodes and the semantics of SELFDESTRUCT. " + "Defaults to the evmVersion the test contract was compiled for, or the latest supported fork", + global_default="", + metavar="VERSION", + ) + ffi: bool = arg( help="allow the usage of FFI to call external functions", global_default=False, diff --git a/src/dolmos/evm_version.py b/src/dolmos/evm_version.py new file mode 100644 index 00000000..88b585b9 --- /dev/null +++ b/src/dolmos/evm_version.py @@ -0,0 +1,124 @@ +# SPDX-License-Identifier: AGPL-3.0 + +""" +EVM version (hard fork) setting (a16z/halmos#128). + +The version determines which opcodes are available: executing an opcode introduced +by a later fork halts the current frame with an invalid-opcode error, as on a node +running that fork. It also selects the semantics of SELFDESTRUCT (EIP-6780, Cancun). + +When no version is given, the `evmVersion` the test contract was compiled for is +used (from the compiler metadata), falling back to the latest supported fork. +""" + +from dolmos.contract import ( + OP_BASEFEE, + OP_BLOBBASEFEE, + OP_BLOBHASH, + OP_CHAINID, + OP_CLZ, + OP_CREATE2, + OP_DELEGATECALL, + OP_EXTCODEHASH, + OP_MCOPY, + OP_PUSH0, + OP_RETURNDATACOPY, + OP_RETURNDATASIZE, + OP_REVERT, + OP_SAR, + OP_SELFBALANCE, + OP_SHL, + OP_SHR, + OP_STATICCALL, + OP_TLOAD, + OP_TSTORE, +) + +# supported forks, in chronological order (solc/foundry names) +EVM_VERSIONS = ( + "homestead", + "tangerineWhistle", + "spuriousDragon", + "byzantium", + "constantinople", + "petersburg", + "istanbul", + "berlin", + "london", + "paris", + "shanghai", + "cancun", + "prague", + "osaka", +) + +LATEST_EVM_VERSION = EVM_VERSIONS[-1] + +# alternative names accepted on input +EVM_VERSION_ALIASES = { + "merge": "paris", + "frontier": "homestead", # frontier-only semantics are not modeled +} + +# fork that introduced each opcode, for opcodes not available since homestead +OPCODE_INTRODUCED_IN = { + OP_DELEGATECALL: "homestead", + OP_REVERT: "byzantium", + OP_RETURNDATASIZE: "byzantium", + OP_RETURNDATACOPY: "byzantium", + OP_STATICCALL: "byzantium", + OP_SHL: "constantinople", + OP_SHR: "constantinople", + OP_SAR: "constantinople", + OP_CREATE2: "constantinople", + OP_EXTCODEHASH: "constantinople", + OP_CHAINID: "istanbul", + OP_SELFBALANCE: "istanbul", + OP_BASEFEE: "london", + OP_PUSH0: "shanghai", + OP_TLOAD: "cancun", + OP_TSTORE: "cancun", + OP_MCOPY: "cancun", + OP_BLOBHASH: "cancun", + OP_BLOBBASEFEE: "cancun", + OP_CLZ: "osaka", +} + + +def normalize_evm_version(name: str) -> str | None: + """Returns the canonical fork name, or None if the name is not supported""" + name = EVM_VERSION_ALIASES.get(name, name) + for version in EVM_VERSIONS: + if version.lower() == name.lower(): + return version + return None + + +def fork_index(version: str) -> int: + return EVM_VERSIONS.index(version) + + +def is_at_least(version: str, fork: str) -> bool: + return fork_index(version) >= fork_index(fork) + + +def unavailable_opcodes(version: str) -> frozenset[int]: + """Opcodes introduced after the given fork""" + index = fork_index(version) + return frozenset( + opcode + for opcode, fork in OPCODE_INTRODUCED_IN.items() + if fork_index(fork) > index + ) + + +def evm_version_from_metadata(contract_json: dict) -> str | None: + """Returns the evmVersion found in the compiler metadata of a build artifact""" + metadata = contract_json.get("metadata") + if not isinstance(metadata, dict): + return None + settings = metadata.get("settings") + if not isinstance(settings, dict): + return None + version = settings.get("evmVersion") + return version if isinstance(version, str) else None diff --git a/src/dolmos/sevm.py b/src/dolmos/sevm.py index 45e7b80b..b63f96a8 100644 --- a/src/dolmos/sevm.py +++ b/src/dolmos/sevm.py @@ -162,6 +162,13 @@ Instruction, mnemonic, ) +from dolmos.evm_version import ( + EVM_VERSIONS, + LATEST_EVM_VERSION, + is_at_least, + normalize_evm_version, + unavailable_opcodes, +) from dolmos.exceptions import ( AddressCollision, DolmosException, @@ -2330,6 +2337,18 @@ def __len__(self) -> int: return len(self.stack) +def resolve_evm_version(name: str) -> str: + if not name: + return LATEST_EVM_VERSION + + version = normalize_evm_version(name) + if version is None: + raise DolmosException( + f"unsupported EVM version: {name} (supported: {', '.join(EVM_VERSIONS)})" + ) + return version + + class SEVM: options: DolmosConfig fun_info: FunctionInfo @@ -2352,6 +2371,11 @@ def __init__(self, options: DolmosConfig, fun_info: FunctionInfo) -> None: is_generic = self.options.storage_layout == "generic" self.storage_model = GenericStorage if is_generic else SolidityStorage + # EVM version (a16z/halmos#128) + self.evm_version = resolve_evm_version(self.options.evm_version) + self.unavailable_opcodes = unavailable_opcodes(self.evm_version) + self.eip6780 = is_at_least(self.evm_version, "cancun") + def div_xy_y(self, w1: Word, w2: Word) -> Word: # return the number of bits required to represent the given value. default = 256 def bitsize(w: Word) -> int: @@ -3645,7 +3669,10 @@ def gen_nested_ite(curr: int) -> BitVecRef: def selfdestruct(self, ex: Exec) -> None: """ - SELFDESTRUCT with the semantics introduced by EIP-6780 (Cancun): + Before Cancun, the account is always deleted at the end of the transaction, and + a balance sent to itself is burnt. + + From Cancun, SELFDESTRUCT follows EIP-6780: - the whole balance of the executing account is sent to the beneficiary; - the account is deleted, at the end of the transaction, only if it was created in the same transaction. In that case, a balance sent to itself @@ -3661,7 +3688,7 @@ def selfdestruct(self, ex: Exec) -> None: balance = BV(ex.balance_of(this)) self.transfer_value(ex, this, beneficiary.as_z3(), balance) - if this in ex.created_in_tx: + if not self.eip6780 or this in ex.created_in_tx: ex.context.output.accounts_to_delete.add(this) ex.halt(data=ByteVec()) @@ -3748,6 +3775,7 @@ def finalize(ex: Exec): # cache config options out of the hot loop no_status = self.options.no_status + unavailable = self.unavailable_opcodes max_depth = self.options.depth print_steps = self.options.print_steps print_mem = self.options.print_mem @@ -3850,6 +3878,12 @@ def finalize(ex: Exec): if print_steps: print(ex.dump(print_mem=print_mem)) + # opcodes introduced after the selected EVM version are invalid + if unavailable and opcode in unavailable: + ex.halt(data=ByteVec(), error=InvalidOpcode(opcode)) + yield from finalize(ex) + continue + # Reordered based on frequency data if OP_PUSH1 <= opcode <= OP_PUSH31: state.push(insn.operand) diff --git a/tests/expected/all.json b/tests/expected/all.json index 6e526ec4..6a493142 100644 --- a/tests/expected/all.json +++ b/tests/expected/all.json @@ -1026,6 +1026,48 @@ "num_bounded_loops": null } ], + "test/EvmVersion.t.sol:EvmVersionDefaultTest": [ + { + "name": "check_all_available()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/EvmVersion.t.sol:EvmVersionPragueTest": [ + { + "name": "check_clz_invalid()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/EvmVersion.t.sol:EvmVersionShanghaiTest": [ + { + "name": "check_tstore_available_fail()", + "exitcode": 1, + "num_models": 1, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_tstore_invalid()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], "test/Expect.t.sol:ExpectTest": [ { "name": "check_expectEmit(uint256,uint256)", @@ -4311,6 +4353,28 @@ "num_bounded_loops": null } ], + "test/SelfDestruct.t.sol:SelfDestructShanghaiDeletedTest": [ + { + "name": "check_preexisting_deleted_after_tx()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], + "test/SelfDestruct.t.sol:SelfDestructShanghaiTest": [ + { + "name": "check_preexisting_alive_until_end_of_tx()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], "test/SelfDestruct.t.sol:SelfDestructTest": [ { "name": "check_balance_moved_fail()", diff --git a/tests/regression/test/EvmVersion.t.sol b/tests/regression/test/EvmVersion.t.sol new file mode 100644 index 00000000..ae4439b5 --- /dev/null +++ b/tests/regression/test/EvmVersion.t.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +// EVM version setting (a16z/halmos#128): opcodes introduced after the selected fork are +// invalid. The probed opcodes run in contracts deployed from raw bytecode, so that the +// test contracts themselves (compiled for the default evm_version) are not affected. +abstract contract EvmVersionBase { + function run(uint256 initcode, uint256 size) internal returns (bool success) { + assembly { + mstore(0, initcode) + let target := create(0, 0, size) + success := call(gas(), target, 0, 0, 0, 0, 0) + } + } + + // runtime: PUSH0 PUSH0 SSTORE STOP + function push0() internal returns (bool) { + return run(0x6004600a5f3960045ff35f5f5500000000000000000000000000000000000000, 14); + } + + // runtime: PUSH1 1 PUSH0 TSTORE STOP + function tstore() internal returns (bool) { + return run(0x6005600a5f3960055ff360015f5d000000000000000000000000000000000000, 15); + } + + // runtime: PUSH1 1 CLZ STOP + function clz() internal returns (bool) { + return run(0x6004600a5f3960045ff360011e00000000000000000000000000000000000000, 14); + } +} + +// default: the evm_version of the build (osaka) +contract EvmVersionDefaultTest is EvmVersionBase { + function check_all_available() public { + assert(push0()); + assert(tstore()); + assert(clz()); + } +} + +/// @custom:dolmos --evm-version prague +contract EvmVersionPragueTest is EvmVersionBase { + function check_clz_invalid() public { + assert(push0()); + assert(tstore()); + assert(!clz()); + } +} + +/// @custom:dolmos --evm-version shanghai +contract EvmVersionShanghaiTest is EvmVersionBase { + function check_tstore_invalid() public { + assert(push0()); + assert(!tstore()); + assert(!clz()); + } + + // expected failure: TSTORE is not available in shanghai + function check_tstore_available_fail() public { + assert(tstore()); + } +} diff --git a/tests/regression/test/SelfDestruct.t.sol b/tests/regression/test/SelfDestruct.t.sol index a2aaba55..7636b9b3 100644 --- a/tests/regression/test/SelfDestruct.t.sol +++ b/tests/regression/test/SelfDestruct.t.sol @@ -132,3 +132,41 @@ contract SelfDestructDeletedTest is Test { assertEq(ret.length, 0); } } + +// before Cancun, SELFDESTRUCT deletes the account at the end of the transaction, +// even if it was not created in that transaction (a16z/halmos#128) +/// @custom:dolmos --evm-version shanghai +contract SelfDestructShanghaiTest { + Destructible preexisting; + address payable constant BENEFICIARY = payable(address(0xbeef)); + + function setUp() public { + preexisting = new Destructible{value: 1 ether}(); + } + + // still alive until the end of the transaction + function check_preexisting_alive_until_end_of_tx() public { + preexisting.destroy(BENEFICIARY); + assert(address(preexisting).balance == 0); + assert(BENEFICIARY.balance == 1 ether); + assert(address(preexisting).code.length > 0); + } +} + +/// @custom:dolmos --evm-version shanghai +contract SelfDestructShanghaiDeletedTest { + Destructible preexisting; + + // the account is created in the constructor (first transaction), and destroyed in setUp + constructor() { + preexisting = new Destructible(); + } + + function setUp() public { + preexisting.destroy(payable(address(0xbeef))); + } + + function check_preexisting_deleted_after_tx() public view { + assert(address(preexisting).code.length == 0); + } +} diff --git a/tests/test_evm_version.py b/tests/test_evm_version.py new file mode 100644 index 00000000..388c57cd --- /dev/null +++ b/tests/test_evm_version.py @@ -0,0 +1,125 @@ +import pytest +from test_sevm import caller, mk_ex, this + +from dolmos.__main__ import with_evm_version +from dolmos.config import ConfigSource, default_config +from dolmos.evm_version import ( + EVM_VERSIONS, + LATEST_EVM_VERSION, + evm_version_from_metadata, + is_at_least, + normalize_evm_version, + unavailable_opcodes, +) +from dolmos.exceptions import DolmosException, InvalidOpcode +from dolmos.sevm import SEVM +from dolmos.utils import EVM + + +def mk_sevm(fun_info, evm_version: str) -> SEVM: + args = default_config().with_overrides( + ConfigSource.command_line, evm_version=evm_version + ) + return SEVM(args, fun_info) + + +def test_versions_are_ordered(): + assert EVM_VERSIONS.index("shanghai") < EVM_VERSIONS.index("cancun") + assert LATEST_EVM_VERSION == "osaka" + assert is_at_least("prague", "cancun") + assert not is_at_least("shanghai", "cancun") + + +@pytest.mark.parametrize( + "name, expected", + [ + ("cancun", "cancun"), + ("Cancun", "cancun"), + ("tangerinewhistle", "tangerineWhistle"), + ("merge", "paris"), + ("unknown", None), + ], +) +def test_normalize(name, expected): + assert normalize_evm_version(name) == expected + + +def test_unavailable_opcodes(): + assert unavailable_opcodes("osaka") == frozenset() + assert unavailable_opcodes("prague") == {EVM.CLZ} + assert unavailable_opcodes("shanghai") == { + EVM.TLOAD, + EVM.TSTORE, + EVM.MCOPY, + EVM.BLOBHASH, + EVM.BLOBBASEFEE, + EVM.CLZ, + } + assert EVM.PUSH0 in unavailable_opcodes("paris") + assert EVM.PUSH0 not in unavailable_opcodes("shanghai") + assert EVM.BASEFEE in unavailable_opcodes("berlin") + assert EVM.CHAINID in unavailable_opcodes("petersburg") + assert EVM.CREATE2 in unavailable_opcodes("byzantium") + assert EVM.REVERT in unavailable_opcodes("spuriousDragon") + + +def test_metadata(): + assert ( + evm_version_from_metadata({"metadata": {"settings": {"evmVersion": "paris"}}}) + == "paris" + ) + assert evm_version_from_metadata({}) is None + assert evm_version_from_metadata({"metadata": "not a dict"}) is None + + +def test_with_evm_version_from_metadata(): + args = with_evm_version( + default_config(), {"metadata": {"settings": {"evmVersion": "shanghai"}}} + ) + assert args.evm_version == "shanghai" + + +def test_with_evm_version_option_wins(): + cli = default_config().with_overrides( + ConfigSource.command_line, evm_version="cancun" + ) + args = with_evm_version(cli, {"metadata": {"settings": {"evmVersion": "paris"}}}) + assert args.evm_version == "cancun" + + +def test_with_evm_version_unknown_in_metadata(): + args = with_evm_version( + default_config(), {"metadata": {"settings": {"evmVersion": "future"}}} + ) + assert args.evm_version == "" # falls back to the latest fork + + +def test_unsupported_version_rejected(fun_info): + with pytest.raises(DolmosException): + mk_sevm(fun_info, "future") + + +@pytest.mark.parametrize( + "hexcode, evm_version, valid", + [ + ("5f00", "shanghai", True), # PUSH0; STOP + ("5f00", "paris", False), + ("60015f5d00", "cancun", True), # PUSH1 1; PUSH0; TSTORE; STOP + ("60015f5d00", "shanghai", False), + ("60011e00", "osaka", True), # PUSH1 1; CLZ; STOP + ("60011e00", "prague", False), + ("4800", "london", True), # BASEFEE; STOP + ("4800", "berlin", False), + ], +) +def test_opcode_availability(hexcode, evm_version, valid, fun_info, solver): + sevm = mk_sevm(fun_info, evm_version) + ex = mk_ex( + bytes.fromhex(hexcode), sevm, solver, sevm.mk_storagedata(), caller, this + ) + [out] = list(sevm.run(ex)) + error = out.context.output.error + if valid: + assert error is None + else: + assert isinstance(error, InvalidOpcode) From 929fe493b3cab242a507eeb2df7507ff58ae132b Mon Sep 17 00:00:00 2001 From: Hugo DE SA PEREIRA PINTO Date: Fri, 25 Sep 2026 23:02:54 +0200 Subject: [PATCH 37/46] fix(#562): pin storage layout in KeccakRefinement test, keep counterexample when refinement is undecided --- src/dolmos/solve.py | 8 ++++ tests/regression/test/KeccakRefinement.t.sol | 5 +++ tests/test_solve.py | 47 ++++++++++++++++++++ 3 files changed, 60 insertions(+) diff --git a/src/dolmos/solve.py b/src/dolmos/solve.py index eee4fc41..0122ea7f 100644 --- a/src/dolmos/solve.py +++ b/src/dolmos/solve.py @@ -731,6 +731,7 @@ def refine_keccak(ctx: PathContext, solver_output: SolverOutput) -> SolverOutput query=SMTQuery(base_smtlib + "".join(facts.values()), ctx.query.assertions), suffix=f".keccak{round + 1}", ) + last_sat_output = solver_output solver_output = solve_low_level(refined_ctx) # an unsat core of the refined query may depend on the (unnamed) keccak facts, @@ -738,6 +739,13 @@ def refine_keccak(ctx: PathContext, solver_output: SolverOutput) -> SolverOutput if solver_output.unsat_core is not None: solver_output = replace(solver_output, unsat_core=None) + # if the refined query cannot be decided (timeout, solver error), keep the last + # counterexample rather than dropping it: only unsat proves its absence + if solver_output.result not in (sat, unsat): + verbose(f" Refined query result: {solver_output.result}") + solver_output = last_sat_output + break + # the model still relies on keccak values that may not exist verbose(" Keccak values of the model are not consistent with keccak256") potential_model = PotentialModel(model=solver_output.model.model, is_valid=False) diff --git a/tests/regression/test/KeccakRefinement.t.sol b/tests/regression/test/KeccakRefinement.t.sol index 1c4631f1..3983cd92 100644 --- a/tests/regression/test/KeccakRefinement.t.sol +++ b/tests/regression/test/KeccakRefinement.t.sol @@ -4,6 +4,11 @@ pragma solidity >=0.8.0 <0.9.0; // a16z/halmos#562: keccak256 is uninterpreted, so the solver may assign to a symbolic // preimage a hash value that differs from the real one. Such counterexamples are checked // against the real keccak256 values, and reported as potentially invalid if they do not agree. +// +// The solidity storage layout is required: the generic layout decodes hashed storage keys +// structurally, so a hash can never be equal to the plain key written in setUp, and the +// counterexample path is pruned during exploration. +/// @custom:dolmos --storage-layout solidity contract KeccakRefinementTest { mapping(uint256 => uint256) balances; diff --git a/tests/test_solve.py b/tests/test_solve.py index f40c67f3..d08d5b83 100644 --- a/tests/test_solve.py +++ b/tests/test_solve.py @@ -1,5 +1,6 @@ import pytest from eth_hash.auto import keccak +from z3 import sat, unknown, unsat from dolmos.solve import ( ModelVariable, @@ -155,3 +156,49 @@ def test_sha3_arg_decls(): "(declare-fun sha3arg_1 () (_ BitVec 512))\n" "(assert (= sha3arg_1 (concat a b)))\n" ) + + +def _refine_keccak_setup(tmp_path, monkeypatch, refined_results): + """runs refine_keccak on a sat model whose keccak preimage is 0, with mocked solver results""" + import dolmos.solve as solve_mod + from dolmos.config import default_config + from dolmos.sevm import SMTQuery + from dolmos.solve import PathContext, PotentialModel, SolverOutput, SolvingContext + + ctx = PathContext( + args=default_config(), + path_id=0, + solving_ctx=SolvingContext(dump_dir=tmp_path), + query=SMTQuery("", []), + sha3_args=((256, "p_x_uint256_00"),), + ) + + def mk_output(result, index): + query_file = str(tmp_path / f"{index}.smt2") + with open(f"{query_file}.out", "w") as f: + f.write(f"(define-fun sha3arg_0 () (_ BitVec 256) (_ bv{index} 256))\n") + model = PotentialModel(model={}, is_valid=True) if result == sat else None + return SolverOutput(result, 0, 0, query_file, model=model) + + outputs = iter(mk_output(r, i + 1) for i, r in enumerate(refined_results)) + monkeypatch.setattr(solve_mod, "solve_low_level", lambda _ctx: next(outputs)) + return solve_mod.refine_keccak(ctx, mk_output(sat, 0)) + + +def test_refine_keccak_unsat(tmp_path, monkeypatch): + # the refined query is unsat: no counterexample under the real keccak values + assert _refine_keccak_setup(tmp_path, monkeypatch, [unsat]).result == unsat + + +def test_refine_keccak_undecided_keeps_counterexample(tmp_path, monkeypatch): + # the refined query times out: the counterexample is kept, as potentially invalid + out = _refine_keccak_setup(tmp_path, monkeypatch, [unknown]) + assert out.result == sat + assert not out.model.is_valid + + +def test_refine_keccak_rounds_exhausted(tmp_path, monkeypatch): + # every round picks a new preimage: potentially invalid after the last round + out = _refine_keccak_setup(tmp_path, monkeypatch, [sat, sat, sat]) + assert out.result == sat + assert not out.model.is_valid From e331db6f5b5a3397e9f550473c48fc267539ab65 Mon Sep 17 00:00:00 2001 From: HugoDowsers <123469473+HugoDowsers@users.noreply.github.com> Date: Sat, 26 Sep 2026 17:58:42 +0200 Subject: [PATCH 38/46] Create specification.md --- specification.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 specification.md diff --git a/specification.md b/specification.md new file mode 100644 index 00000000..d6463d51 --- /dev/null +++ b/specification.md @@ -0,0 +1,36 @@ + + +# Modeling choices # + +## CLZ operator ## + + +## Self Destruct ## + +## Keccak256 256 operator ## + +Simplified explanation + +Dolmos models keccak256 using a family of uninterpreted functions, f_sha3_N, with one function for each input size. Instead of encoding the whole hash algorithm bit by bit—which would make SMT solving impractical—Dolmos only adds a few basic properties to these functions. + +When the input is concrete, Dolmos computes the real keccak256 value and stores the input/output pair for that execution path. This makes it possible to recover the preimage of a concrete storage slot and decode it into its structure, such as the base slot, mapping keys, and offsets. When the input is symbolic, the hash remains uninterpreted and is constrained only by a small set of axioms. An inverse function guarantees injectivity, so two different inputs cannot have the same hash. Hash values are also assumed to be non-zero and smaller than 2^256 − 2^64, preventing a struct or array offset from overflowing when added to a hashed storage slot. + +These assumptions are sound in practice, but they are deliberately incomplete: they do not formally connect the symbolic function to the real keccak256. As a result, the solver can invent a hash value. For example, it may choose an input whose symbolic hash matches a storage key written in setUp(), producing a counterexample that cannot actually be reproduced (halmos#562). + +Dolmos handles this lazily through counterexample-guided refinement. When the solver finds a satisfiable model, Dolmos reads the concrete preimages chosen by the model, computes their real keccak256 values, adds facts such as f_sha3_N(v) = keccak256(v), and runs the solver again. Because these facts are true for the real hash function, a real counterexample is never removed. Therefore, an unsat result proves that no counterexample exists under the stated assumptions. If the model remains inconsistent after a bounded number of refinement rounds, or if refinement cannot determine whether it is valid, Dolmos still reports it but marks it as potentially invalid instead of silently discarding it. + +Two additional design choices complete this approach. First, the optimizer may fold a keccak256 computation into a precomputed constant. If the resulting storage location is later decoded structurally, the value stored at the raw location is moved to the corresponding structured location (halmos#579). Second, the alternative generic storage layout can decode hashed storage keys purely structurally. This implicitly assumes that a hash cannot be equal to an unrelated constant. + +Example: halmos#562 + +The issue halmos#562, reproduced in KeccakRefinement.t.sol, shows why this refinement mechanism is necessary. In setUp(), the contract executes balances[key] = 50, where key is a concrete value. The test then checks that balances[uint256(keccak256(abi.encodePacked(amt)))] != 50 for every symbolic amt. + +When balances[key] is written, its storage location is keccak256(key . 0). Because the input is concrete, Dolmos computes the real hash and records it. It can therefore decode the storage location as (slot 0, key) rather than treating it as an arbitrary 256-bit value. + +During the test, however, keccak256(amt) is symbolic. Dolmos represents it as h = f_sha3_256(amt), so the read becomes a lookup at (slot 0, h). The read returns 50 if h = key. The current axioms do not prevent this: injectivity only says that two different inputs cannot have the same hash. The solver can therefore invent a model such as amt = 0 together with f_sha3_256(0) = key. It reports sat, but this is a false counterexample because the real keccak256(0) is not equal to key. + +Refinement then starts. Dolmos takes the model's preimage, 0, computes the real value of keccak256(0), and adds the constraint f_sha3_256(0) = keccak256(0). The previous model is now impossible. The solver may then try another value, for example amt = 1, with another invented hash. Dolmos adds the corresponding real hash and repeats the process. After the configured three refinement rounds (--keccak-refinement-rounds), the counterexample is reported as potentially invalid, rather than being presented as a valid exploit. + +The same mechanism behaves differently when a real preimage exists. In check_keccak_known_preimage, the test searches for an amt such that keccak256(amt) == keccak256(42). The solver proposes amt = 42, and the refinement step confirms that this matches the real hash. The counterexample is therefore validated immediately. + +The example also shows the effect of the two other modeling choices. With the generic storage layout, h is structurally understood as the hash of amt. It therefore cannot simply be treated as the unrelated constant key, so the problematic path can be eliminated before SMT solving. This is why the test uses the Solidity storage layout. Finally, if key were a compile-time constant, the optimizer could fold keccak256(key . 0) into a PUSH32 constant (halmos#579). The value would initially be stored under the raw location and then moved to the structured location when that location is later decoded. This is why the test keeps key in a state variable. From 53580aee9c8d8791e008ce52071b0a35bef6f83e Mon Sep 17 00:00:00 2001 From: Hugo DE SA PEREIRA PINTO Date: Mon, 12 Oct 2026 13:37:52 +0200 Subject: [PATCH 39/46] Issue 215 --- CHANGELOG.md | 4 + README.md | 7 + src/dolmos/cheatcodes.py | 67 +++++++ src/dolmos/fs.py | 162 +++++++++++++++++ src/dolmos/utils.py | 4 - tests/expected/all.json | 128 ++++++++++++++ tests/regression/foundry.toml | 8 + tests/regression/test/File.t.sol | 92 ++++++++++ tests/regression/test/data/binary.bin | Bin 0 -> 3 bytes tests/regression/test/data/hello.txt | 2 + tests/regression/test/data/initcode.hex | 1 + tests/regression/test/data/private/secret.txt | 1 + tests/test_fs.py | 164 ++++++++++++++++++ 13 files changed, 636 insertions(+), 4 deletions(-) create mode 100644 src/dolmos/fs.py create mode 100644 tests/regression/test/File.t.sol create mode 100644 tests/regression/test/data/binary.bin create mode 100644 tests/regression/test/data/hello.txt create mode 100644 tests/regression/test/data/initcode.hex create mode 100644 tests/regression/test/data/private/secret.txt create mode 100644 tests/test_fs.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ddeb363..ef6ae98d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ All notable changes to dolmos are documented here. The format follows ### Added +- file cheatcodes `vm.readFile`, `vm.readFileBinary`, `vm.parseBytes` and + `vm.projectRoot`, e.g. to deploy Huff bytecode read from a file; reads honor + `fs_permissions` from `foundry.toml` with foundry's semantics, and + `vm.parseBytes` accepts exactly what foundry accepts (halmos#213) - `vm.expectRevert` (all overloads, including the reverter address and `count` variants), `vm.expectPartialRevert`, `vm.expectEmit` and `vm.expectEmitAnonymous` (including the emitter address and `count` variants), diff --git a/README.md b/README.md index 389f6784..a188784a 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,13 @@ does, therefore agrees with the executor. `vm.resetNonce`, with foundry's semantics: `setNonce` only raises a nonce, `resetNonce` gives 0 to EOAs and 1 to accounts with code. +**File cheatcodes.** `vm.readFile`, `vm.readFileBinary`, `vm.parseBytes` and +`vm.projectRoot`, so a test can deploy bytecode compiled outside solc (Huff, for +instance) from a file. Reads are subject to `fs_permissions` in `foundry.toml`, +resolved with `forge config` and applied as foundry does (paths normalized +against the project root, most specific permission wins, denied by default), so +a test that reads a file passes or fails the same way under `forge test`. + **Fusaka / Cancun opcodes.** `CLZ` (EIP-7939), with a concrete fast path and a logarithmic symbolic encoding, plus `BLOBHASH` and `BLOBBASEFEE` (EIP-4844). diff --git a/src/dolmos/cheatcodes.py b/src/dolmos/cheatcodes.py index b2e93d71..eb0f0b31 100644 --- a/src/dolmos/cheatcodes.py +++ b/src/dolmos/cheatcodes.py @@ -50,6 +50,7 @@ parse_expect_emit, parse_expect_revert, ) +from dolmos.fs import parse_hex_bytes, read_file from dolmos.logs import debug from dolmos.mapper import BuildOut from dolmos.utils import ( @@ -602,6 +603,44 @@ def env_int(arg, **kwargs): return ByteVec(int_val) +def concrete_string_argument(arg, arg_idx: int, cheatcode: str) -> str: + value = extract_string_argument(arg, arg_idx) + if not isinstance(value, str): + raise NotConcreteError(f"{cheatcode}: expected a concrete string, got {value}") + return value + + +def read_file_cheatcode(sevm, arg) -> ByteVec: + """vm.readFile(string path) returns (string)""" + path = concrete_string_argument(arg, 0, "vm.readFile") + data = read_file(sevm.options.root, path, "vm.readFile") + try: + # like foundry, reject files that are not valid utf-8 text + data.decode("utf-8") + except UnicodeDecodeError as e: + msg = f"vm.readFile: {path} is not valid UTF-8 (use vm.readFileBinary): {e}" + raise DolmosException(msg) from e + return encode_tuple_bytes(data) + + +def read_file_binary_cheatcode(sevm, arg) -> ByteVec: + """vm.readFileBinary(string path) returns (bytes)""" + path = concrete_string_argument(arg, 0, "vm.readFileBinary") + return encode_tuple_bytes(read_file(sevm.options.root, path, "vm.readFileBinary")) + + +def parse_bytes_cheatcode(arg) -> ByteVec: + """vm.parseBytes(string) returns (bytes)""" + value = concrete_string_argument(arg, 0, "vm.parseBytes") + return encode_tuple_bytes(parse_hex_bytes(value, "vm.parseBytes")) + + +def project_root_cheatcode(sevm) -> ByteVec: + """vm.projectRoot() returns (string)""" + root = os.path.abspath(sevm.options.root) + return encode_tuple_bytes(root.encode("utf-8")) + + def abi_encode_array_words(values: list[Bool | Address | Word]) -> ByteVec: result = ByteVec() @@ -1121,6 +1160,18 @@ class hevm_cheat_code: # bytes4(keccak256("envExists(string)")) env_exists_sig: int = 0xCE8365F9 + # bytes4(keccak256("readFile(string)")) + read_file_sig: int = 0x60F9BB11 + + # bytes4(keccak256("readFileBinary(string)")) + read_file_binary_sig: int = 0x16ED7BC4 + + # bytes4(keccak256("parseBytes(string)")) + parse_bytes_sig: int = 0x8F5D232D + + # bytes4(keccak256("projectRoot()")) + project_root_sig: int = 0xD930A0E6 + @staticmethod def handle(sevm, ex, arg: ByteVec, stack) -> ByteVec | None: funsig: int = int_of(arg[:4].unwrap(), "symbolic hevm cheatcode") @@ -1625,6 +1676,22 @@ def handle(sevm, ex, arg: ByteVec, stack) -> ByteVec | None: elif funsig == hevm_cheat_code.env_exists_sig: return env_exists(arg) + # vm.readFile(string) returns (string) + elif funsig == hevm_cheat_code.read_file_sig: + return read_file_cheatcode(sevm, arg) + + # vm.readFileBinary(string) returns (bytes) + elif funsig == hevm_cheat_code.read_file_binary_sig: + return read_file_binary_cheatcode(sevm, arg) + + # vm.parseBytes(string) returns (bytes) + elif funsig == hevm_cheat_code.parse_bytes_sig: + return parse_bytes_cheatcode(arg) + + # vm.projectRoot() returns (string) + elif funsig == hevm_cheat_code.project_root_sig: + return project_root_cheatcode(sevm) + # vm.expectRevert(...) / vm.expectPartialRevert(...) elif funsig in REVERT_SIGNATURES: ex.context.expectations.expect_revert(parse_expect_revert(funsig, arg)) diff --git a/src/dolmos/fs.py b/src/dolmos/fs.py new file mode 100644 index 00000000..6ea88b32 --- /dev/null +++ b/src/dolmos/fs.py @@ -0,0 +1,162 @@ +# SPDX-License-Identifier: AGPL-3.0 + +""" +Filesystem access for the file cheatcodes (vm.readFile, vm.readFileBinary, ...). + +Access is governed by the project's `fs_permissions`, with the same semantics as +Foundry, so that a test reading a file behaves identically under `forge test` and +under dolmos: + +- permissions are read from the resolved forge configuration (`forge config --json`), + so profiles and environment overrides are honored; +- paths, both requested and permitted, are resolved against the project root and + normalized lexically (`a/../b` becomes `b`; symlinks are not followed); +- the most specific permission (longest matching path) wins, so a nested + `{ access = "none", path = "./data/private" }` overrides a broader read grant; +- without any matching permission, access is denied. +""" + +import json +import os +import re +import subprocess +from dataclasses import dataclass +from functools import cache + +from dolmos.exceptions import DolmosException +from dolmos.logs import debug, warn + +# foundry's default when fs_permissions is not configured +DEFAULT_FS_PERMISSIONS: tuple[dict, ...] = ({"access": "read", "path": "out"},) + +# `forge config --json` serializes "read-write" as true and "none" as false +_ACCESS_ALIASES = { + True: "read-write", + False: "none", + "true": "read-write", + "false": "none", +} + +_READABLE = frozenset({"read", "read-write"}) + + +@dataclass(frozen=True) +class FsPermission: + access: str # "none" | "read" | "write" | "read-write" + path: str # absolute and normalized + + @property + def can_read(self) -> bool: + return self.access in _READABLE + + +def resolve_path(root: str, path: str) -> str: + """Absolute, lexically normalized path; relative paths are taken from root.""" + return os.path.normpath(os.path.join(os.path.abspath(root), path)) + + +def parse_fs_permissions(root: str, raw: list | tuple) -> tuple[FsPermission, ...]: + permissions = [] + for entry in raw: + access = entry["access"] + access = _ACCESS_ALIASES.get(access, access) + if isinstance(access, str): + access = access.lower() + permissions.append(FsPermission(access, resolve_path(root, entry["path"]))) + return tuple(permissions) + + +@cache +def load_fs_permissions(root: str) -> tuple[FsPermission, ...]: + """Resolved fs_permissions of the forge project at root (cached per root).""" + + cmd = ["forge", "config", "--json", "--root", root] + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + raw = json.loads(result.stdout).get("fs_permissions") or [] + except (OSError, subprocess.CalledProcessError, ValueError) as e: + warn( + f"could not resolve fs_permissions with `{' '.join(cmd)}` ({e}); " + "falling back to foundry's default (read access to `out` only)" + ) + raw = DEFAULT_FS_PERMISSIONS + + permissions = parse_fs_permissions(root, raw) + debug(f"fs_permissions for {root}: {permissions}") + return permissions + + +def find_permission( + permissions: tuple[FsPermission, ...], abs_path: str +) -> FsPermission | None: + """The most specific permission covering abs_path, if any.""" + + best = None + for permission in permissions: + base = permission.path + covered = abs_path == base or abs_path.startswith(base.rstrip(os.sep) + os.sep) + if covered and (best is None or len(base) > len(best.path)): + best = permission + return best + + +def ensure_readable( + root: str, + path: str, + cheatcode: str, + permissions: tuple[FsPermission, ...] | None = None, +) -> str: + """ + Returns the absolute path to read, or raises if fs_permissions deny reading it. + """ + + if permissions is None: + permissions = load_fs_permissions(root) + + abs_path = resolve_path(root, path) + permission = find_permission(permissions, abs_path) + if permission is None or not permission.can_read: + shown = os.path.relpath(abs_path, os.path.abspath(root)) + if shown.startswith(os.pardir): + shown = abs_path + raise DolmosException( + f"{cheatcode}: the path {shown} is not allowed to be accessed for read " + "operations (see fs_permissions in foundry.toml)" + ) + + return abs_path + + +def read_file(root: str, path: str, cheatcode: str) -> bytes: + """Raw content of the file at path, subject to fs_permissions.""" + + abs_path = ensure_readable(root, path, cheatcode) + try: + with open(abs_path, "rb") as f: + return f.read() + except OSError as e: + raise DolmosException(f"{cheatcode}: failed to read {abs_path}: {e}") from e + + +_HEX_DIGITS = re.compile(r"[0-9a-fA-F]*") + + +def parse_hex_bytes(value: str, cheatcode: str = "vm.parseBytes") -> bytes: + """ + Parses a hex string the way Foundry's vm.parseBytes does: an optional + lowercase `0x` prefix followed by an even number of hex digits. Whitespace + is rejected, and the empty string is only accepted as `0x`. + """ + + digits = value[2:] if value.startswith("0x") else value + + if not value: + reason = 'missing hex prefix ("0x") for hex string' + elif not _HEX_DIGITS.fullmatch(digits): + reason = "invalid hex character" + elif len(digits) % 2: + reason = "odd number of digits" + else: + return bytes.fromhex(digits) + + raise DolmosException(f'{cheatcode}: failed parsing "{value}" as bytes: {reason}') diff --git a/src/dolmos/utils.py b/src/dolmos/utils.py index 9ea39893..0f32bc0b 100644 --- a/src/dolmos/utils.py +++ b/src/dolmos/utils.py @@ -1626,7 +1626,6 @@ def wrapper(*args, **kwargs): 0x592151F0: "parseToml(string)", 0x37736E08: "parseToml(string,string)", 0x812A44B2: "parseTomlKeys(string,string)", - 0xD930A0E6: "projectRoot()", 0x47EAF474: "prompt(string)", 0x1E279D41: "promptSecret(string)", 0x69CA02B7: "promptSecretUint(string)", @@ -1668,7 +1667,6 @@ def wrapper(*args, **kwargs): 0x28A249B0: "getLabel(address)", 0xC6CE059D: "parseAddress(string)", 0x974EF924: "parseBool(string)", - 0x8F5D232D: "parseBytes(string)", 0x087E6E81: "parseBytes32(string)", 0x42346C5E: "parseInt(string)", 0xFA91454D: "parseUint(string)", @@ -1687,8 +1685,6 @@ def wrapper(*args, **kwargs): 0x7D15D019: "isDir(string)", 0xE0EB04D4: "isFile(string)", 0xC4BC59E0: "readDir(string)", - 0x60F9BB11: "readFile(string)", - 0x16ED7BC4: "readFileBinary(string)", 0x70F55728: "readLine(string)", 0x9F5684A2: "readLink(string)", 0xF1AFE04D: "removeFile(string)", diff --git a/tests/expected/all.json b/tests/expected/all.json index 6a493142..cd3bb459 100644 --- a/tests/expected/all.json +++ b/tests/expected/all.json @@ -1549,6 +1549,134 @@ "num_bounded_loops": null } ], + "test/File.t.sol:FileTest": [ + { + "name": "check_deployedFromFile(uint256)", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_parseBytes()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_parseBytes_oddLength()", + "exitcode": 3, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_parseBytes_symbolic(string)", + "exitcode": 3, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_parseBytes_whitespace()", + "exitcode": 3, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_projectRoot()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_readFile()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_readFileBinary()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_readFile_escape()", + "exitcode": 3, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_readFile_invalidUtf8()", + "exitcode": 3, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_readFile_missing()", + "exitcode": 3, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_readFile_nestedDenial()", + "exitcode": 3, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_readFile_normalizedPath()", + "exitcode": 0, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + }, + { + "name": "check_readFile_notPermitted()", + "exitcode": 3, + "num_models": 0, + "models": null, + "num_paths": null, + "time": null, + "num_bounded_loops": null + } + ], "test/Foundry.t.sol:DeepFailer": [ { "name": "check_fail_cheatcode()", diff --git a/tests/regression/foundry.toml b/tests/regression/foundry.toml index 6e09532c..6f289892 100644 --- a/tests/regression/foundry.toml +++ b/tests/regression/foundry.toml @@ -9,5 +9,13 @@ optimizer = true force = false +# file cheatcodes (test/File.t.sol): `out` is foundry's default, test/data is read by +# the tests, and test/data/private checks that a nested denial overrides it +fs_permissions = [ + { access = "read", path = "./out" }, + { access = "read", path = "./test/data" }, + { access = "none", path = "./test/data/private" }, +] + # compile options used by dolmos (to prevent unnecessary recompilation when running forge test and dolmos together) extra_output = ["storageLayout", "metadata"] diff --git a/tests/regression/test/File.t.sol b/tests/regression/test/File.t.sol new file mode 100644 index 00000000..c16ec89e --- /dev/null +++ b/tests/regression/test/File.t.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity >=0.8.0 <0.9.0; + +import "forge-std/Test.sol"; + +/// File cheatcodes (halmos#213): vm.readFile, vm.readFileBinary, vm.parseBytes, vm.projectRoot. +/// Access follows fs_permissions in foundry.toml: test/data is readable, test/data/private is not. +contract FileTest is Test { + address deployed; + + // initcode of a contract that returns 42 for any call (as compiled by huff, say) + bytes constant INITCODE = hex"600a600c600039600a6000f3602a60005260206000f3"; + + function setUp() public { + // the scenario from halmos#213: deploy bytecode that is not compiled by solc + bytes memory initcode = vm.parseBytes(vm.readFile("test/data/initcode.hex")); + address addr; + assembly { + addr := create(0, add(initcode, 0x20), mload(initcode)) + } + require(addr != address(0), "deployment failed"); + deployed = addr; + } + + function check_deployedFromFile(uint256 x) public { + (bool success, bytes memory ret) = deployed.call(abi.encode(x)); + assert(success); + assert(abi.decode(ret, (uint256)) == 42); + } + + function check_readFile() public view { + // returned as is, trailing newline included + string memory text = vm.readFile("test/data/hello.txt"); + assert(keccak256(bytes(text)) == keccak256("hello\nworld\n")); + } + + function check_readFileBinary() public view { + bytes memory data = vm.readFileBinary("test/data/binary.bin"); + assert(keccak256(data) == keccak256(hex"00ff80")); + } + + function check_readFile_normalizedPath() public view { + string memory text = vm.readFile("test/data/private/../hello.txt"); + assert(keccak256(bytes(text)) == keccak256("hello\nworld\n")); + } + + function check_projectRoot() public view { + string memory path = string.concat(vm.projectRoot(), "/test/data/hello.txt"); + assert(keccak256(bytes(vm.readFile(path))) == keccak256("hello\nworld\n")); + } + + function check_parseBytes() public view { + assert(keccak256(vm.parseBytes("0x600a6000F3")) == keccak256(hex"600a6000f3")); + assert(keccak256(vm.parseBytes("600a6000f3")) == keccak256(hex"600a6000f3")); + assert(vm.parseBytes("0x").length == 0); + assert(keccak256(vm.parseBytes(vm.readFile("test/data/initcode.hex"))) == keccak256(INITCODE)); + } + + // the following must error, as they do under forge test + + function check_readFile_notPermitted() public view { + vm.readFile("foundry.toml"); + } + + function check_readFile_nestedDenial() public view { + vm.readFile("test/data/private/secret.txt"); + } + + function check_readFile_escape() public view { + vm.readFile("test/data/../../foundry.toml"); + } + + function check_readFile_missing() public view { + vm.readFile("test/data/missing.txt"); + } + + function check_readFile_invalidUtf8() public view { + vm.readFile("test/data/binary.bin"); + } + + function check_parseBytes_whitespace() public view { + vm.parseBytes("0x600a\n"); + } + + function check_parseBytes_oddLength() public view { + vm.parseBytes("0x123"); + } + + function check_parseBytes_symbolic(string memory s) public view { + vm.parseBytes(s); + } +} diff --git a/tests/regression/test/data/binary.bin b/tests/regression/test/data/binary.bin new file mode 100644 index 0000000000000000000000000000000000000000..d90dae2bbeb97246b9570981c3eac7b6c403df7b GIT binary patch literal 3 KcmZSh-v9stfdPO3 literal 0 HcmV?d00001 diff --git a/tests/regression/test/data/hello.txt b/tests/regression/test/data/hello.txt new file mode 100644 index 00000000..94954abd --- /dev/null +++ b/tests/regression/test/data/hello.txt @@ -0,0 +1,2 @@ +hello +world diff --git a/tests/regression/test/data/initcode.hex b/tests/regression/test/data/initcode.hex new file mode 100644 index 00000000..d0d9f6a3 --- /dev/null +++ b/tests/regression/test/data/initcode.hex @@ -0,0 +1 @@ +0x600a600c600039600a6000f3602a60005260206000f3 \ No newline at end of file diff --git a/tests/regression/test/data/private/secret.txt b/tests/regression/test/data/private/secret.txt new file mode 100644 index 00000000..536aca34 --- /dev/null +++ b/tests/regression/test/data/private/secret.txt @@ -0,0 +1 @@ +secret \ No newline at end of file diff --git a/tests/test_fs.py b/tests/test_fs.py new file mode 100644 index 00000000..166ad8f7 --- /dev/null +++ b/tests/test_fs.py @@ -0,0 +1,164 @@ +import os +import subprocess + +import pytest + +from dolmos import fs +from dolmos.exceptions import DolmosException +from dolmos.fs import ( + FsPermission, + ensure_readable, + find_permission, + parse_fs_permissions, + parse_hex_bytes, + resolve_path, +) + +ROOT = "/project" + + +def perms(*entries) -> tuple[FsPermission, ...]: + return parse_fs_permissions(ROOT, [{"access": a, "path": p} for a, p in entries]) + + +def test_parse_fs_permissions_access_aliases(): + # `forge config --json` serializes "read-write" as true and "none" as false + parsed = perms(("read", "./a"), (True, "b"), (False, "c"), ("write", "/abs/d")) + assert [(p.access, p.path) for p in parsed] == [ + ("read", "/project/a"), + ("read-write", "/project/b"), + ("none", "/project/c"), + ("write", "/abs/d"), + ] + assert [p.can_read for p in parsed] == [True, True, False, False] + + +def test_resolve_path_is_lexical(): + assert resolve_path(ROOT, "data/../secret/s.txt") == "/project/secret/s.txt" + assert resolve_path(ROOT, "./data/x") == "/project/data/x" + assert resolve_path(ROOT, "/elsewhere/x") == "/elsewhere/x" + assert resolve_path(ROOT, "../x") == "/x" + + +def test_find_permission_most_specific_wins(): + p = perms(("read", "./data"), ("none", "./data/private")) + assert find_permission(p, "/project/data/x.txt").access == "read" + assert find_permission(p, "/project/data/private/x.txt").access == "none" + assert find_permission(p, "/project/data/private").access == "none" + # order of declaration does not matter + p = perms(("none", "./data/private"), ("read", "./data")) + assert find_permission(p, "/project/data/private/x.txt").access == "none" + + +def test_find_permission_matches_path_components_only(): + p = perms(("read", "./data")) + assert find_permission(p, "/project/database.txt") is None + assert find_permission(p, "/project/data-other/x") is None + assert find_permission(p, "/project/data") is not None + + +def test_find_permission_root(): + p = perms(("read", "./")) + assert find_permission(p, "/project/anything/at/all").access == "read" + assert find_permission(p, "/outside") is None + + +@pytest.mark.parametrize( + "path", + [ + "foundry.toml", + "data/../foundry.toml", + "data/private/secret.txt", + "../outside.txt", + "/etc/passwd", + "out-of-tree/data/x", + ], +) +def test_ensure_readable_denied(path): + p = perms(("read", "./data"), ("none", "./data/private")) + with pytest.raises(DolmosException, match="not allowed to be accessed"): + ensure_readable(ROOT, path, "vm.readFile", p) + + +def test_ensure_readable_allowed(): + p = perms(("read", "./data"), ("read-write", "/shared"), ("write", "./logs")) + assert ensure_readable(ROOT, "data/x", "vm.readFile", p) == "/project/data/x" + assert ensure_readable(ROOT, "/shared/y", "vm.readFile", p) == "/shared/y" + with pytest.raises(DolmosException): + ensure_readable(ROOT, "logs/z", "vm.readFile", p) + + +def test_ensure_readable_no_permissions(): + with pytest.raises(DolmosException): + ensure_readable(ROOT, "anything", "vm.readFile", ()) + + +def test_load_fs_permissions_falls_back_to_foundry_default(monkeypatch, tmp_path): + def failing_run(*args, **kwargs): + raise FileNotFoundError("forge") + + monkeypatch.setattr(subprocess, "run", failing_run) + fs.load_fs_permissions.cache_clear() + try: + root = str(tmp_path) + assert fs.load_fs_permissions(root) == ( + FsPermission("read", os.path.join(root, "out")), + ) + finally: + fs.load_fs_permissions.cache_clear() + + +def test_read_file(monkeypatch, tmp_path): + (tmp_path / "data").mkdir() + (tmp_path / "data" / "crlf.txt").write_bytes(b"a\r\nb\n") + (tmp_path / "secret.txt").write_bytes(b"secret") + + root = str(tmp_path) + monkeypatch.setattr( + fs, + "load_fs_permissions", + lambda r: parse_fs_permissions(r, [{"access": "read", "path": "data"}]), + ) + + # returned as is, no newline translation + assert fs.read_file(root, "data/crlf.txt", "vm.readFile") == b"a\r\nb\n" + + with pytest.raises(DolmosException, match="failed to read"): + fs.read_file(root, "data/missing.txt", "vm.readFile") + + with pytest.raises(DolmosException, match="failed to read"): + fs.read_file(root, "data", "vm.readFile") # a directory + + with pytest.raises(DolmosException, match="not allowed"): + fs.read_file(root, "secret.txt", "vm.readFile") + + +@pytest.mark.parametrize( + "value, expected", + [ + ("0x", b""), + ("0x00ff", b"\x00\xff"), + ("00ff", b"\x00\xff"), + ("0xABcd", b"\xab\xcd"), + ("0x600a600c600039600a6000f3", bytes.fromhex("600a600c600039600a6000f3")), + ], +) +def test_parse_hex_bytes(value, expected): + assert parse_hex_bytes(value) == expected + + +@pytest.mark.parametrize( + "value, reason", + [ + ("", "missing hex prefix"), + ("0x123", "odd number of digits"), + ("0x6001\n", "invalid hex character"), + (" 0xab", "invalid hex character"), + ("0x ab", "invalid hex character"), + ("0XAB", "invalid hex character"), + ("0xzz", "invalid hex character"), + ], +) +def test_parse_hex_bytes_invalid(value, reason): + with pytest.raises(DolmosException, match=reason): + parse_hex_bytes(value) From 26974be011769ac1e81ed912fea5bcb8ae0a32e7 Mon Sep 17 00:00:00 2001 From: "hugo@dowsers.financee" Date: Wed, 14 Oct 2026 10:16:46 +0200 Subject: [PATCH 40/46] feat: --pretty-counterexamples (-pcex) for exploitable counterexamples (halmos#280, #369, #398, #419) With -pcex, counterexamples are printed so that they can be acted upon: - test arguments decoded with the ABI of the test: lengths of dynamic arrays, bytes and strings applied, values rendered by type (decimal with hints such as 1e18 or 2**255, checksummed addresses with the contract and test variable behind them, strings) - bytes holding calldata decoded with the ABIs of the compiled contracts, and calls made with calldata built by svm.createCalldata listed (halmos#369) - bytes4 values rendered as selectors, 0x00000000 as the empty selector of plain ether transfers (halmos#419) - invariant call sequences with concrete senders, values, arguments and timestamps - a forge test replaying each counterexample: typed calls through `this` and through the state variables of the test holding the targets, low-level calls otherwise - duplicate counterexamples hidden and counted (halmos#398) - potentially invalid counterexamples relying on the EXP abstraction checked against the real exponentiation: constant exponents expanded by square and multiply, constant bases tabulated, real values added at the operands of the model, up to 3 rounds (keccak values checked in the same loop). The counterexample is then valid, proven spurious (unsat), or reported with the reason (halmos#280) Also fixes --early-exit, which reported the queries cancelled after the first counterexample as solver errors and saved them as failed queries (halmos#398). Without the option, the output is unchanged. Verdicts and model counts are the same with and without it on tests/regression, except for counterexamples proven spurious. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FT71T6kg8kz9yamjguBUb8 --- CHANGELOG.md | 17 + docs/getting-started.md | 2 + docs/pretty-counterexamples.md | 140 ++ docs/warnings.md | 7 + src/dolmos/__main__.py | 173 ++- src/dolmos/config.py | 12 + src/dolmos/pcex.py | 1785 +++++++++++++++++++++++++ src/dolmos/solve.py | 436 +++++- tests/expected/all.json | 47 + tests/regression/test/PrettyCex.t.sol | 63 + tests/test_pcex.py | 940 +++++++++++++ 11 files changed, 3611 insertions(+), 11 deletions(-) create mode 100644 docs/pretty-counterexamples.md create mode 100644 src/dolmos/pcex.py create mode 100644 tests/regression/test/PrettyCex.t.sol create mode 100644 tests/test_pcex.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ef6ae98d..9939cc18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,9 +37,26 @@ All notable changes to dolmos are documented here. The format follows and `SELFDESTRUCT` deletes the account unconditionally before Cancun. Defaults to the `evmVersion` the test contract was compiled for (foundry's `evm_version`), or to the latest supported fork, `osaka` (halmos#128) +- `--pretty-counterexamples` (`-pcex`) prints counterexamples that can be acted upon; see + docs/pretty-counterexamples.md: + - test arguments decoded with the ABI of the test, with the lengths of dynamic values + applied and values rendered by type (decimal, checksummed addresses, strings) + - `bytes` holding calldata decoded with the ABIs of the compiled contracts, and calls + made with calldata built by `svm.createCalldata` listed (halmos#369) + - `bytes4` values rendered as selectors, `0x00000000` as the empty selector of plain + ether transfers (halmos#419) + - invariant call sequences with concrete senders, values, arguments and timestamps + - a forge test replaying each counterexample + - duplicate counterexamples hidden, and counted (halmos#398) + - potentially invalid counterexamples relying on the approximation of `EXP` checked + against the real exponentiation: they are confirmed, replaced by a valid one, or + proven spurious; otherwise the reason is printed (halmos#280) ### Fixed +- `--early-exit` no longer reports the queries cancelled after the first counterexample as + solver errors (`executor has been shutdown`), nor saves them as failed queries + (halmos#398) - SHA256, RIPEMD160 and MODEXP precompiles with symbolic input no longer crash with a z3 sort mismatch (input sizes were given in bytes instead of bits). Concrete inputs are now evaluated exactly; MODEXP follows EIP-198 (zero-padded diff --git a/docs/getting-started.md b/docs/getting-started.md index 7fca3510..3ae44f35 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -223,6 +223,8 @@ For our example, dolmos will identify an input combination where the sender addr **Tips:** +- Run with `--pretty-counterexamples` (`-pcex`) to get counterexamples decoded with the ABI (arguments, calldata, selectors, invariant call sequences), without duplicates, and with a forge test that replays each of them. See [pretty counterexamples](pretty-counterexamples.md). + - Dolmos focuses solely on assertion violations (i.e., revert with `Panic(1)`), disregarding other revert cases. This means that dolmos 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 dolmos will _not_ report any counterexamples by default. In that case, run with `--invalid-as-failure` (the default when the build contains Vyper contracts), or use a custom assertion that reverts with `Panic(1)` upon failure, as shown below: diff --git a/docs/pretty-counterexamples.md b/docs/pretty-counterexamples.md new file mode 100644 index 00000000..cbda2ef4 --- /dev/null +++ b/docs/pretty-counterexamples.md @@ -0,0 +1,140 @@ +# Pretty counterexamples + +By default, a counterexample is printed as the raw solver model: one line per symbol, with +the full hexadecimal value of every word. A `bytes` argument of length 65 is printed as the +1024 bytes of its largest length candidate, a `bytes4` selector as `0x00`, and the same +values are repeated for every failing path. + +`--pretty-counterexamples` (short: `-pcex`) prints counterexamples that can be acted upon: + +``` +dolmos -pcex +``` + +It can also be set in `dolmos.toml` (`pretty-counterexamples = true`), or for a contract or +a function with `/// @custom:dolmos --pretty-counterexamples`. The verdicts of the tests +and the JSON output are the same as without the option, except for counterexamples that +are proven spurious (see [Potentially invalid counterexamples](#potentially-invalid-counterexamples)). + +## What it changes + +- **Arguments decoded with the ABI of the test.** The lengths of dynamic arrays, `bytes` and + `string` are applied, and values are printed by type: decimal integers (with a readable + form such as `1e18`, `2**255` or `type(uint256).max` when there is one), checksummed + addresses (with the contract deployed there and the state variable of the test holding + it), quoted strings, `true`/`false`. Structs and arrays are flattened to one line per + value (`p.a = 7`, `calls[1] = …`). +- **Calldata decoded** (halmos#369). `bytes` values whose first 4 bytes are the selector of a + function of a compiled contract are decoded with its ABI, e.g. + `Token.transfer(to: 0x…, amount: 2) + 956 trailing zero bytes`. Calls made by the test + with calldata built from other symbols (e.g. by `svm.createCalldata`) are listed under + `Calls made with symbolic calldata`. +- **Selectors** (halmos#419). `bytes4` values are printed with the function they select, and + `0x00000000` as the empty selector of plain ether transfers (`receive()` or + `fallback()`), unless some contract has a function with that selector. +- **Invariant call sequences with concrete values**: target, decoded arguments, `msg.sender`, + `msg.value`, `tx.origin` and `block.timestamp` of each call. The symbolic trace is still + printed with `-vv`. +- **A forge test replaying the counterexample**, to paste into the test contract. It fails + if the counterexample is real. +- **Duplicates hidden** (halmos#398). A counterexample with the same values as one already + printed for the test is counted but not printed; the number of hidden duplicates is + printed after the result of the test. `--early-exit` stops at the first valid + counterexample. +- **Potentially invalid counterexamples are checked**, and the reason is printed when they + remain potentially invalid (halmos#280), see below. + +## Example + +From `tests/regression/test/PrettyCex.t.sol`: + +```solidity +function check_pcex_calldata(bytes calldata data) external { + (bool ok,) = address(token).call(data); + assert(!ok || token.balanceOf(address(this)) == 1000); +} +``` + +Without the option, the counterexample is the 2048 hex digits of `data` (its largest length +candidate) and the length symbol. With it: + +``` +Counterexample: check_pcex_calldata(bytes) + data = PcexToken.transfer(to: 0x0000000000000000000000000000000000000000, amount: 512) + 956 trailing zero bytes + 0xa9059cbb000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002 + 957 zero bytes (1024 bytes) +Reproduce: add to PrettyCexTest (test/PrettyCex.t.sol), then `forge test --mt test_pcex_check_pcex_calldata_1 -vvvv` (the test fails if the counterexample is real) + function test_pcex_check_pcex_calldata_1() public { + this.check_pcex_calldata(abi.encodePacked(hex"a9059cbb000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002", new bytes(957))); + } +``` + +For an invariant test, the call sequence is printed with concrete values: + +``` +Counterexample: invariant_pcex_balance() + Sequence: + [1] PcexToken(token).transfer(to: 0xFFA9385be102ac3EAc297483Dd6233d62b3e1496, amount: 492) + msg.sender: 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496 (PrettyCexTest), tx.origin: 0x0000000000000000000000000000000000000000 + then invariant_pcex_balance() at block.timestamp: 9223372036854775808 +Reproduce: add to PrettyCexTest (test/PrettyCex.t.sol), then `forge test --mt test_pcex_invariant_pcex_balance_1 -vvvv` (the test fails if the counterexample is real) + function test_pcex_invariant_pcex_balance_1() public { + vm.prank(0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496, address(0)); + token.transfer(0xFFA9385be102ac3EAc297483Dd6233d62b3e1496, 492); + vm.warp(9223372036854775808); + this.invariant_pcex_balance(); + } +``` + +## Reproduction tests + +The generated function calls the test function through `this`, with the values of the +counterexample. When the arguments cannot be written as Solidity literals (a value that is +not a valid ABI encoding of its type, e.g. a `uint8` above 255, or an overloaded test +function), it replays the exact calldata with a low-level call instead. + +Invariant sequences are replayed with `vm.prank`, `vm.deal` and `vm.warp`. A target is +called through the state variable of the test that holds it (`vault.deposit(…)`), so that +the call goes to the instance deployed by forge. Dolmos deploys contracts at its own +addresses (`0xaaaa…`, `0xcccc…`), so a target that is not held by a state variable of the +test is called at the dolmos address, with a note to replace it. + +Values that the test creates itself with `svm.create*` cheatcodes (and the calldata it +builds with `svm.createCalldata`) cannot be injected: they are listed as comments at the +top of the function, to replace the cheatcodes with. + +## Potentially invalid counterexamples + +Dolmos approximates some operations with uninterpreted functions, so a model may rely on +values that the real operation never produces. Such counterexamples are reported as +*potentially invalid*. Without the option, the reason is not given (halmos#280). + +With the option, the operands and the result of every exponentiation that dolmos +approximates (`x ** y` with a symbolic exponent, or with a symbolic base and a constant +exponent above `--smt-exp-by-const`) are compared with the real `**`: + +- if they agree, the counterexample is valid; +- otherwise the query is solved again with facts that hold for the real `**`: constant + exponents are expanded by square and multiply, constant bases are tabulated (exactly for + an even base), and the real value is added at the operands chosen by the model. Every + real counterexample is kept, so `unsat` proves that there is none, and the test passes. + This is repeated up to 3 times, plus `--keccak-refinement-rounds` times for the keccak + values checked in the same loop. + +For example, `assert(x ** 3 != 27)` gives a potentially invalid counterexample `x = 8` +without the option, and the valid counterexample `x = 3` with it. When the model still +disagrees, the reason is printed: + +```solidity +function check_pow(uint256 x, uint256 y) external pure { + vm.assume(x % 2 == 0 && y > 0); + assert(x ** y != 3); +} +``` + +``` +Counterexample (potentially invalid): check_pow(uint256,uint256) + reason: EXP is approximated: x ** y (symbolic base and exponent): the model takes 10 ** 16 = 3 instead of 10000000000000000 (constrain the operands with vm.assume (e.g. exponent < 256) or make the base or the exponent constant) + x = 10 + y = 16 +(see https://github.com/Dowsers/dolmos/blob/main/docs/warnings.md#counterexample-invalid) +``` diff --git a/docs/warnings.md b/docs/warnings.md index f86550f6..1617beda 100644 --- a/docs/warnings.md +++ b/docs/warnings.md @@ -29,6 +29,13 @@ reproduce the failure. This usually comes from approximations in the symbolic model (hash functions, unbounded arrays, precompiles). The test is still reported as failing; inspect the counterexample before trusting it. +With `--pretty-counterexamples`, counterexamples relying on the approximation of +exponentiation are checked against the real `**` (and replaced by a valid +counterexample, or proven spurious, when possible), and the reason is printed when +they remain potentially invalid, e.g. a symbolic base and exponent, or keccak256 +values that differ from the real hash function. See +[pretty counterexamples](pretty-counterexamples.md#potentially-invalid-counterexamples). + ## counterexample-unknown The solver could not decide whether a failing path is feasible within diff --git a/src/dolmos/__main__.py b/src/dolmos/__main__.py index 6a36a776..ddd410d1 100644 --- a/src/dolmos/__main__.py +++ b/src/dolmos/__main__.py @@ -95,6 +95,13 @@ warn_code, ) from dolmos.mapper import BuildOut, DeployAddressMapper +from dolmos.pcex import ( + CexLayout, + RenderedCex, + collect_layout, + render_counterexample, + repro_function, +) from dolmos.processes import ExecutorRegistry, ShutdownError from dolmos.sevm import ( EMPTY_BALANCE, @@ -123,6 +130,7 @@ PathContext, SolverOutput, dirname, + mk_exp_apps, mk_sha3_args, solve_end_to_end, solve_low_level, @@ -981,6 +989,17 @@ def handle_assertion_violation( query: SMTQuery = ex.path.to_smt2(args) + # with --pretty-counterexamples, collect what is needed to render the counterexample + # (on this thread, as it reads z3 objects), and the EXP applications to be checked + layout, exp_apps = None, None + if args.pretty_counterexamples: + try: + layout = collect_layout(ctx, ex, self.is_invariant, self.is_probe) + except Exception as e: + debug(f"could not collect the counterexample layout: {e!r}") + if "f_evm_exp_256" in query.smtlib: + exp_apps = mk_exp_apps(ex.path.conditions) + # beware: because this object crosses thread boundaries, we must be careful to # avoid any reference to z3 objects path_ctx = PathContext( @@ -989,6 +1008,7 @@ def handle_assertion_violation( query=query, solving_ctx=ctx.solving_ctx, sha3_args=mk_sha3_args(ex.sha3s) if args.keccak_refinement_rounds else (), + exp_apps=exp_apps, ) # ShutdownError may be raised here and will be handled by the caller @@ -999,33 +1019,46 @@ def handle_assertion_violation( ex=ex, path_ctx=path_ctx, description=description, + layout=layout, ) ) self.submitted_futures.append(solve_future) - def _get_solver_output(self, future: Future, path_ctx: PathContext) -> SolverOutput: + def _get_solver_output( + self, future: Future, path_ctx: PathContext + ) -> SolverOutput | None: + """ + Returns the output of the solver, or None if solving was cancelled because the + executor has been shut down (e.g. by --early-exit once a counterexample is found). + """ path_id, query_file = path_ctx.path_id, str(path_ctx.dump_file) if self.ctx.solving_ctx.executor.is_shutdown(): # if the thread pool is in the process of shutting down, # we want to stop processing remaining models/timeouts/errors, etc. - e = "executor has been shutdown" - return SolverOutput.from_error(e, path_id=path_id, query_file=query_file) + return None if e := future.exception(): - if not is_benign_solving_error(e): - error(f"encountered exception during assertion solving: {e!r}") + if is_benign_solving_error(e): + return None + error(f"encountered exception during assertion solving: {e!r}") return SolverOutput.from_error(e, path_id=path_id, query_file=query_file) try: return future.result() except Exception as e: - if not is_benign_solving_error(e): - error(f"encountered exception during assertion solving: {e!r}") + if is_benign_solving_error(e): + return None + error(f"encountered exception during assertion solving: {e!r}") return SolverOutput.from_error(e, path_id=path_id, query_file=query_file) def _solve_end_to_end_callback( - self, future: Future, ex: Exec, path_ctx: PathContext, description: str + self, + future: Future, + ex: Exec, + path_ctx: PathContext, + description: str, + layout: CexLayout | None = None, ) -> None: """ Callback function for handling solver results. @@ -1034,6 +1067,7 @@ def _solve_end_to_end_callback( future: The Future object containing the solver result ex: The execution state description: Optional description of counterexample + layout: what is needed to render the counterexample (--pretty-counterexamples) """ # beware: this function may be called from threads other than the main thread, # so we must be careful to avoid referencing any z3 objects / contexts @@ -1041,7 +1075,11 @@ def _solve_end_to_end_callback( ctx = self.ctx args = ctx.args - solver_output: SolverOutput = self._get_solver_output(future, path_ctx) + solver_output: SolverOutput | None = self._get_solver_output(future, path_ctx) + if solver_output is None: + # cancelled, e.g. by --early-exit: neither an error nor a result + return + ctx.solver_outputs.append(solver_output) result, model = solver_output.result, solver_output.model path_id = solver_output.path_id @@ -1065,6 +1103,10 @@ def _solve_end_to_end_callback( if model is None: return + if args.pretty_counterexamples: + self._report_pretty_counterexample(ex, solver_output, description, layout) + return + # mark this probe as reported to avoid duplicate reporting # note: message.fun_info is used instead of ctx.info because the function context for probes is dummy. if self.is_probe: @@ -1103,6 +1145,113 @@ def _solve_end_to_end_callback( if sequence := ctx.call_sequences[path_id]: print(f"Sequence:\n{sequence}") + def _report_pretty_counterexample( + self, + ex: Exec, + solver_output: SolverOutput, + description: str | None, + layout: CexLayout | None, + ) -> None: + """Reports a counterexample with --pretty-counterexamples (see dolmos.pcex)""" + # beware: called from the solver threads, see _solve_end_to_end_callback() + ctx = self.ctx + args = ctx.args + model, path_id = solver_output.model, solver_output.path_id + + rendered = None + if layout is not None: + try: + rendered = render_counterexample(layout, model.model) + except Exception as e: + debug(f"could not render the counterexample: {e!r}") + if args.debug: + traceback.print_exc() + + fun_sig = (layout.fun_sig if layout else ctx.info.sig) or "" + if rendered is not None: + body = rendered.body + else: + body = str(model).lstrip("\n").split("\n") + + pcex = ctx.pcex + with pcex.lock: + if model.is_valid: + ctx.valid_counterexamples.append(model) + else: + ctx.invalid_counterexamples.append(model) + + # mark this probe as reported to avoid duplicate reporting + if self.is_probe: + ctx.contract_ctx.probes_reported.add(ex.context.message.fun_info) + + # counterexamples with the same values as one already shown are only counted + if pcex.admit("\n".join(body), model.is_valid): + self._print_pretty_counterexample( + solver_output, description, layout, rendered, body, fun_sig + ) + + if model.is_valid: + # add the stacks from the temporary flamegraph to the global one + if self.flamegraph_enabled and self.is_invariant: + call_flamegraph.stacks.extend( + self.potential_flamegraphs[path_id].stacks + ) + + # we have a valid counterexample, so we are eligible for early exit + if args.early_exit: + debug(f"Shutting down {ctx.info.name}'s solver executor") + ctx.solving_ctx.executor.shutdown(wait=False) + + def _print_pretty_counterexample( + self, + solver_output: SolverOutput, + description: str | None, + layout: CexLayout | None, + rendered: RenderedCex | None, + body: list[str], + fun_sig: str, + ) -> None: + """Prints a counterexample shown by _report_pretty_counterexample()""" + ctx = self.ctx + args = ctx.args + model, path_id = solver_output.model, solver_output.path_id + + lines = [] + if description: + lines.append(description) + + if args.verbose >= VERBOSITY_TRACE_COUNTEREXAMPLE: + pid_str = f" #{path_id}" if args.verbose >= VERBOSITY_TRACE_PATHS else "" + lines.append(f"Trace{pid_str}:") + lines.append(ctx.traces[path_id].rstrip("\n")) + + if model.is_valid: + lines.append(color_error(f"Counterexample: {fun_sig}")) + else: + reason = ( + solver_output.invalid_reason + or "relies on an approximation of the EVM semantics" + ) + lines.append(color_warn(f"Counterexample (potentially invalid): {fun_sig}")) + lines.append(color_warn(f" reason: {reason}")) + + lines.extend(body) + + # the symbolic call sequence of invariant testing, replaced by the concrete one + sequence = ctx.call_sequences.get(path_id) + if sequence and ( + rendered is None or args.verbose >= VERBOSITY_TRACE_COUNTEREXAMPLE + ): + lines.append(f"Sequence (symbolic):\n{sequence.rstrip()}") + + if rendered is not None and rendered.repro and model.is_valid: + lines.extend(repro_function(layout, rendered, ctx.pcex.shown)) + + if not model.is_valid: + lines.append(f"(see {COUNTEREXAMPLE_INVALID.url()})") + + print("\n".join(lines)) + def _save_failed_query( self, path_id: int, solver_output: SolverOutput, failure_type: str ) -> None: @@ -1342,6 +1491,12 @@ def run_test(ctx: FunctionContext) -> TestResult: f"bounds: [{', '.join([str(x) for x in dyn_params])}])" ) + if args.pretty_counterexamples and (hidden := ctx.pcex.hidden): + print( + f" {hidden} duplicate counterexample{'s' if hidden > 1 else ''} not shown " + "(same values as a counterexample above)" + ) + if exitcode in (Exitcode.TIMEOUT.value, Exitcode.EXCEPTION.value): # print query directory information failure_type = "timeout" if exitcode == Exitcode.TIMEOUT.value else "error" diff --git a/src/dolmos/config.py b/src/dolmos/config.py index 91812da9..4c674988 100644 --- a/src/dolmos/config.py +++ b/src/dolmos/config.py @@ -510,6 +510,18 @@ class Config: group=debugging, ) + pretty_counterexamples: bool = arg( + help=( + "print exploitable counterexamples: arguments decoded with the ABI, calldata and " + "selectors decoded, concrete call sequences, a forge test replaying each " + "counterexample, duplicates hidden, and potentially invalid counterexamples " + "checked against the real EXP semantics, with the reason when they remain invalid" + ), + global_default=False, + group=debugging, + short="pcex", + ) + dump_smt_queries: bool = arg( help="dump SMT queries for assertion violations", global_default=False, diff --git a/src/dolmos/pcex.py b/src/dolmos/pcex.py new file mode 100644 index 00000000..e1e9c2bf --- /dev/null +++ b/src/dolmos/pcex.py @@ -0,0 +1,1785 @@ +# SPDX-License-Identifier: AGPL-3.0 + +""" +Pretty counterexamples, enabled with --pretty-counterexamples (-pcex). + +The raw output of a counterexample is the solver model: one line per symbol, with the full +hex value of every word, e.g. 1024 bytes for a `bytes` argument whose length is 65. This module +turns the model into something that can be acted upon: + +- the arguments of the test are decoded with its ABI: the lengths of dynamic arguments are + applied, and values are rendered by type (decimal amounts, checksummed addresses, strings); +- `bytes` values that hold calldata are decoded with the ABIs of the compiled contracts, + e.g. `Token.transfer(to: 0x…, amount: 2)` (halmos#369); +- `bytes4` values are rendered as function selectors, including the empty selector 0x00000000 + of plain ether transfers (halmos#419); +- invariant call sequences are rendered with their concrete senders, values and arguments; +- a forge test that replays the counterexample is generated; +- duplicate counterexamples are hidden (halmos#398). + +Potentially invalid counterexamples are checked against the real EXP by the solver loop of +solve.py (see refine_exp, halmos#280). + +Threading: rendering runs in the callbacks of the solver threads, so nothing here may touch +z3 objects, except the collect_* functions, which run on the main thread when a potential +assertion violation is found, and return plain Python data. +""" + +import json +import re +import threading +from collections.abc import Iterable +from dataclasses import dataclass, field + +from eth_hash.auto import keccak + +# a raw model variable has .value (int) and .size_bits (int); see solve.ModelVariable +Model = dict + +WORD_BITS = 256 +WORD_MODULUS = 2**256 + +# selector of plain ether transfers and of calls with empty calldata +EMPTY_SELECTOR = b"\x00\x00\x00\x00" + +# limits, to keep the output readable +MAX_INLINE_ITEMS = 8 +MAX_HEX_BYTES = 160 +MAX_DECODE_DEPTH = 2 +MAX_ARRAY_LENGTH = 4096 + + +# +# symbol names +# + +# p____, created for ABI encoded arguments (see calldata.py); +# p__length__ for the length of a dynamic array, bytes or string +p_symbol_pattern = re.compile( + r"^p_(?P.*)_(?P[a-z]+[0-9]*)_(?P[0-9a-f]{7})_(?P\d+)$" +) + +# dolmos_