diff --git a/.gitignore b/.gitignore index 2660c26..51a9dc9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,8 @@ __pycache__/ *.py[cod] *.egg-info/ -src/sevm/_revm*.so -src/sevm/_revm*.pyd +src/chasm_debugger/_revm*.so +src/chasm_debugger/_revm*.pyd .eggs/ build/ dist/ diff --git a/AGENTS.md b/AGENTS.md index 91bad67..bb643c1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,10 +1,10 @@ -# AGENTS.md — sevm +# AGENTS.md — Chasm Context for any agent working in this directory. Read before editing. ## What this is -`sevm` is a fullscreen, gdb-compatible interactive debugger for Solidity, running on +`chasm` is a fullscreen, gdb-compatible interactive debugger for Solidity, running on REVM. It stops *inside* a running transaction with the frame still alive: read uncommitted state, call view functions, rewrite a stack operand before the opcode consumes it, mutate storage through Solidity, and force out-of-gas at an exact @@ -15,19 +15,19 @@ core is shared by the Python bridge and the headless JSON-RPC server. ## Layout (src layout) -Every package `__init__.py` carries a map of its own modules, and `src/sevm/__init__.py` +Every package `__init__.py` carries a map of its own modules, and `src/chasm_debugger/__init__.py` maps the whole tree. Read those first; `tests/test_layout.py` fails if one goes stale. ``` -sevm/ +debugger/ ├── Cargo.toml # Rust workspace: core, Python bridge, headless server -├── pyproject.toml # metadata, deps, `sevm` entry points, pytest + Maturin config +├── pyproject.toml # metadata, deps, `chasm` entry points, pytest + Maturin config ├── uv.lock # pinned resolution ├── README.md # landing page and usage ├── rust/ # REVM core, PyO3 bridge, headless JSON-RPC server ├── docs/ # commands, expressions, assembly, Foundry reference -├── src/sevm/ # the package (import as `sevm`) -│ ├── cli.py # arg parsing + `main()`; `.py` vs `.sol` dispatch in `sevm run` +├── src/chasm_debugger/ # the package (import as `chasm_debugger`) +│ ├── cli.py # arg parsing + `main()`; `.py` vs `.sol` dispatch in `chasm run` │ ├── session/ # source debugger over REVM: revm, snapshots, framelocals, │ │ # code, events │ ├── commands/ # the gdb command layer: processor + one module per verb group @@ -36,22 +36,22 @@ sevm/ │ ├── assembly/ # builtins, parser, execute │ ├── cheatcodes/ # registry, cheats, assertions, args, console │ ├── locals/ # layout, index, values -│ ├── tui/ # app, pane, panes, layout, opcodes, theme, sevm.tcss +│ ├── tui/ # app, pane, panes, layout, opcodes, theme, chasm.tcss │ ├── cache.py # on-disk build cache: unit hashing, partial rebuilds -│ ├── artifacts.py # forge-shaped `out/sevm//.json` +│ ├── artifacts.py # forge-shaped `out/chasm//.json` │ ├── libs.py # dependency resolution: imports -> repo -> clone -> remapping │ ├── foundry.py # Foundry test runner: resolve project, discover, REVM driver │ ├── provider.py # synchronous Web3 provider backed by the REVM session -│ ├── revm_server.py # `sevm-engine` headless-process entry point +│ ├── revm_server.py # `chasm-engine` headless-process entry point │ ├── frames.py, srcmap.py, decode.py, disasm.py, dispatch.py -│ ├── doctor.py # `sevm doctor`: compiler, runtimes, caches, overrides +│ ├── doctor.py # `chasm doctor`: compiler, runtimes, caches, overrides │ ├── breakpoints.py, clipboard.py │ └── console.py # plain-text frontend (`--console`) ├── tests/ # one file per layer + conftest/harness/tui_harness └── examples/ # runnable, self-contained projects ``` -`sevm run` dispatches by extension: `.py` attaches to a web3 driver (below); `.sol` +`chasm run` dispatches by extension: `.py` attaches to a web3 driver (below); `.sol` compiles + runs a Foundry test. Both paths resolve dependencies the same way. With no `-m` filter it debugs every test in the file: a fresh deploy + `setUp` + test per function, a breakpoint on each body, opening at the first @@ -70,7 +70,7 @@ surface and v1 limits. ## Library resolution (libs.py) -sevm ships no forge-std. `compile_foundry_project` walks the import graph +chasm ships no forge-std. `compile_foundry_project` walks the import graph (`libs.import_closure`), and anything unresolved is installed: prefix -> `libs.ALIASES`, else npm registry metadata (`repository.url`) -> `git clone --depth 1 --branch ` into `/lib/`, with the remapping derived from where the imported @@ -101,7 +101,7 @@ from ~710ms to ~28ms. Inside the test contract itself the closure is nearly the project, so the cost stands. The second invocation is skipped outright for a bare identifier. `Evaluator._known_type` -takes a bound local's type from the parameter sevm declares for it and a state variable's +takes a bound local's type from the parameter chasm declares for it and a state variable's from solc's `typeString` in the AST, searching bindings first and then `linearizedBaseContracts`, which is the order Solidity itself shadows in. Both strings are what the probe's diagnostic would have named, so this is a lookup and not a second type @@ -157,7 +157,7 @@ way; do not add `sys.path` hacks. resolved solc version, `optimize`, `evm_version`, the remappings, the output selection and every source's content hash. A hit loads `/.json.gz` (solc's standard-JSON output, `errors` stripped) and goes straight to `_build_project` without invoking solc. The -cache is `cache/sevm/` inside a Foundry root, else `~/.cache/sevm/projects/`, so a +cache is `cache/chasm/` inside a Foundry root, else `~/.cache/chasm/projects/`, so a plain directory is never written into. A miss with a usable base (same settings, same source *key set*) builds partially: solc @@ -170,13 +170,13 @@ sees is unchanged, file ids, analysis and source maps come out identical to a fu `base_for` requires an identical key set because adding or removing a file shifts them. A build also leaves forge-shaped artifacts (`artifacts.write_out`): one JSON per contract -under `/sevm//.json`, `out` taken from foundry.toml. The `sevm/` -nesting is load-bearing, not tidiness: sevm compiles optimizer-off, and overwriting +under `/chasm//.json`, `out` taken from foundry.toml. The `chasm/` +nesting is load-bearing, not tidiness: chasm compiles optimizer-off, and overwriting `out//.json` would leave forge's own cache calling it fresh, so the -next `forge test` would run sevm's build. Contract names that collide across sources with +next `forge test` would run chasm's build. Contract names that collide across sources with the same basename get a `.1` suffix, and `sourceName` says which source each came from. Artifacts are written when a build happens, or when a hit finds the `out` tree gone; they -carry no `metadata`, which is the one forge field sevm does not request from solc. +carry no `metadata`, which is the one forge field chasm does not request from solc. Every cache failure is a miss, never an error. Tests must never touch `~/.cache`: `conftest.isolated_cache` points `XDG_CACHE_HOME` at tmp per test, which is also what lets @@ -195,18 +195,18 @@ the standing instruction. Everything runs through uv. Do not call bare `pip`/`python` against a system interpreter. ```bash -uv sync # editable install of sevm + dev group into .venv -uv run sevm --help # run the CLI -uv run sevm run --contracts tests/contracts examples/debug_bank.py # fullscreen TUI -uv run sevm run --console --contracts tests/contracts examples/debug_bank.py -uv run sevm compile tests/contracts # what sevm sees +uv sync # editable install of chasm + dev group into .venv +uv run chasm --help # run the CLI +uv run chasm run --contracts tests/contracts examples/debug_bank.py # fullscreen TUI +uv run chasm run --console --contracts tests/contracts examples/debug_bank.py +uv run chasm compile tests/contracts # what chasm sees uv run pytest -q # test suite (751 tests; ~2.5 min, solc compile is the slow part) -SEVM_NETWORK_TESTS=1 uv run pytest -q -m network # 4 more, against the real forge-std/npm +CHASM_NETWORK_TESTS=1 uv run pytest -q -m network # 4 more, against the real forge-std/npm uv run ruff check src tests examples # lint (config in pyproject [tool.ruff]) uv run ruff format src tests examples # format (line length 90) uv run mypy src # type check (pragmatic config, not strict) uv build # wheel + sdist -uv tool install . # install the `sevm` command globally +uv tool install . # install the `chasm` command globally ``` Style is enforced by ruff, not by hand: run `ruff format` and `ruff check --fix` before @@ -217,7 +217,7 @@ fixture tuple and using only some fields is normal). Every module carries `from __future__ import annotations`, so annotations use builtin generics (`dict`, `list`, `X | None`), not `typing.Dict`/`Optional`. -CLI shape: `sevm {run,compile,doctor}`. Options go **before** the target script; everything +CLI shape: `chasm {run,compile,doctor}`. Options go **before** the target script; everything after the script is forwarded to it. Recognition of user contracts is by bytecode (runtime code matched against compiled artifacts, metadata hash stripped, immutables masked), so an unmodified web3 script works as a `run` target. @@ -251,7 +251,7 @@ for directory trees only. ## Dependencies Declared in `pyproject.toml` and the Cargo manifests. Rule: every third-party Python -package imported under `src/sevm` is a direct dependency rather than left to transitive +package imported under `src/chasm_debugger` is a direct dependency rather than left to transitive resolution. `test_packaging.py` walks the imports and fails if one goes undeclared, mapping import names such as `solcx` to their distributions. Dev-only tools (`pytest`, `textual-dev`) live in the PEP 735 `[dependency-groups] dev`, @@ -259,11 +259,11 @@ not in the runtime deps, and are not shipped in the wheel. Do not add `web3[tester]`, `eth-tester`, or `py-evm`: Web3 calls run through `provider.RevmProvider`. `eth-hash[pycryptodome]` is named directly so the keccak backend -sevm relies on is not left to a transitive dependency. +chasm relies on is not left to a transitive dependency. Two files are package data, both loaded relative to their module and included by the -Maturin build: the Textual stylesheet `src/sevm/tui/sevm.tcss` and the node -driver `src/sevm/compile/soljson.js`. If you move or rename either, update `CSS_PATH` in +Maturin build: the Textual stylesheet `src/chasm_debugger/tui/chasm.tcss` and the node +driver `src/chasm_debugger/compile/soljson.js`. If you move or rename either, update `CSS_PATH` in `tui/app.py` or `DRIVER` in `compile/wasm.py`, and re-check `uv build` still bundles it. Nothing else is: forge-std is no longer vendored. @@ -277,14 +277,14 @@ release list publishes, and proves the result by running `--version` — a downl is not evidence it can execute, since the Linux builds are glibc-linked and musl or NixOS cannot run them. An existing `~/.solcx` binary, one of Foundry's under `~/.svm`, or a matching `solc` on PATH is used before anything is downloaded, and `--solc-binary` / -`SEVM_SOLC` overrides the lot (that binary then also names the version, because it is what +`CHASM_SOLC` overrides the lot (that binary then also names the version, because it is what runs and the version keys the build cache). py-solc-x still *invokes* solc through `compile_standard(solc_binary=...)`. Where no native build exists or the one downloaded cannot execute, `compile/wasm.py` runs -solc's Emscripten build (`soljson.js`, cached under `~/.cache/sevm/solc-wasm/`) through +solc's Emscripten build (`soljson.js`, cached under `~/.cache/chasm/solc-wasm/`) through node, which is the only option on musl, NixOS and unpublished architectures — it is a JS -bundle, not a WASI module, so no Python runtime can load it. `SEVM_NODE` names the +bundle, not a WASI module, so no Python runtime can load it. `CHASM_NODE` names the runtime. Output is identical to the native compiler's, which the build cache depends on since it is keyed on the solc version and not on the backend, so `solcbin.Compiler` is what the rest of the package holds rather than a path. Tests and examples use solc @@ -298,14 +298,14 @@ what the rest of the package holds rather than a path. Tests and examples use so `Debugger` helper; `conftest.py` holds the fixtures every file shares and `tui_harness.py` the Textual pilot helpers. Test files import them with `from harness import ...`, which works because `pyproject.toml` sets -`[tool.pytest.ini_options] pythonpath = ["tests"]`; the `sevm` package itself is imported +`[tool.pytest.ini_options] pythonpath = ["tests"]`; the `chasm_debugger` package is imported from the editable install. Fixtures are self-contained under `tests/contracts/` (`Bank.sol`, `Locals.sol`, `Vault.sol`) — do not reach outside the project for a contract. `test_cheatcodes.py` covers the cheat registry itself. Every registered signature is exercised once and `test_every_cheat_is_exercised` fails if one is added without a case. The expected values are not invented, each was read off a run of the same call under real -`forge test`, so the file doubles as sevm's differential record of Foundry behaviour. The +`forge test`, so the file doubles as chasm's differential record of Foundry behaviour. The generated `vm.assert*` overloads are driven from the signature rather than a hand-written table, which is the only way 116 of them stay covered. Cheats that touch VM state are proven a second time end to end in `test_foundry.py`, where they reach a real REVM @@ -320,7 +320,7 @@ provisioning against faked release lists and downloads: which list each platform version comes from, the checksum gate, and that an already-installed binary (including svm's) is preferred to a download. `test_wasm.py` covers the fallback with a stub runtime in place of node, and its one network test compiles a fixture both ways and requires the -documents to be equal. `test_doctor.py` covers which facts `sevm doctor` calls problems, +documents to be equal. `test_doctor.py` covers which facts `chasm doctor` calls problems, since that is what its exit status is. `test_packaging.py` guards `pyproject.toml`: no dependency may ask for the `tester` extra, every package the sources import is declared, and keccak answers on the pycryptodome backend. `test_layout.py` guards the package tree itself: every module @@ -339,7 +339,7 @@ and its own `lib/forge-std/src/`, which must be used untouched). Both are copied end by running each test to completion (the test's own `assertEq`s prove the effect). Tests marked `network` reach the real forge-std, npm and openzeppelin repositories, and -are skipped unless `SEVM_NETWORK_TESTS=1`. One of them asserts sevm implements every +are skipped unless `CHASM_NETWORK_TESTS=1`. One of them asserts chasm implements every `assert*` the current forge-std declares, so a new overload upstream fails the suite rather than surfacing as "unimplemented cheatcode" at run time. @@ -357,7 +357,7 @@ rather than surfacing as "unimplemented cheatcode" at run time. debuggable artifact. 8. Implement the `vm.assert*` cheats as real comparisons. forge-std calls the `*Decimal` and `*ApproxEq*` forms unconditionally, so a blanket revert fails passing assertions. -9. Bind `msg.data`/`msg.sig` from the frame. The injected `__sevm_eval` is reached by a +9. Bind `msg.data`/`msg.sig` from the frame. The injected `__chasm_eval` is reached by a real call, so read directly they report *that* call's calldata (`0x365a2820` plus the bound locals), silently and plausibly wrong. 10. Apply a delegate-call prank to both caller identity and target/storage context in the @@ -384,7 +384,7 @@ pushes a new internal frame; and REVERT pauses only surface while a debugger is REVM 43.0.2, Rust 1.92.0, Maturin 1.15.0, web3 7.16.0, py-solc-x 2.0.5, solc 0.8.28, git 2.x, forge-std 1.16.2, and CPython 3.12. `requires-python = ">=3.10"`; the Rust workspace requires 1.91+. All 832 offline tests pass as of 2026-09-14 (5 more with -`SEVM_NETWORK_TESTS=1`), covering every registered cheatcode +`CHASM_NETWORK_TESTS=1`), covering every registered cheatcode against values taken from real forge, Foundry multi-test coverage, library install and remapping derivation, the assertion engine, the Yul assembly surface, the build cache and its artifacts, the snapshot refresh after a mutation, the dispatcher and selector layer, diff --git a/Cargo.lock b/Cargo.lock index 4658ece..ae8713b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -768,6 +768,34 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" +[[package]] +name = "chasm-revm-core" +version = "0.1.0" +dependencies = [ + "crossbeam-channel", + "revm", +] + +[[package]] +name = "chasm-revm-headless" +version = "0.1.0" +dependencies = [ + "chasm-revm-core", + "revm", + "serde", + "serde_json", +] + +[[package]] +name = "chasm-revm-python" +version = "0.1.0" +dependencies = [ + "chasm-revm-core", + "chasm-revm-headless", + "pyo3", + "revm", +] + [[package]] name = "chrono" version = "0.4.45" @@ -2591,34 +2619,6 @@ dependencies = [ "syn 3.0.5", ] -[[package]] -name = "sevm-revm-core" -version = "0.1.0" -dependencies = [ - "crossbeam-channel", - "revm", -] - -[[package]] -name = "sevm-revm-headless" -version = "0.1.0" -dependencies = [ - "revm", - "serde", - "serde_json", - "sevm-revm-core", -] - -[[package]] -name = "sevm-revm-python" -version = "0.1.0" -dependencies = [ - "pyo3", - "revm", - "sevm-revm-core", - "sevm-revm-headless", -] - [[package]] name = "sha2" version = "0.10.9" diff --git a/Cargo.toml b/Cargo.toml index 6c55e3b..426a021 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,5 +10,5 @@ license = "MIT" [workspace.dependencies] revm = { version = "=43.0.2", default-features = false, features = ["std"] } -sevm-revm-core = { path = "rust/core" } -sevm-revm-headless = { path = "rust/headless" } +chasm-revm-core = { path = "rust/core" } +chasm-revm-headless = { path = "rust/headless" } diff --git a/README.md b/README.md index b44ec57..7cc8fba 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# sevm +# Chasm -`sevm` is an interactive, gdb-style Solidity/EVM debugger on REVM, built for red-team dynamic analysis. +Chasm is an interactive, gdb-style Solidity/EVM debugger on REVM, built for red-team dynamic analysis. - **Full control**: Stepping through Solidity is only half of what this debugger does. Here you are `root` on the EVM, free to rewrite state, the stack, memory, storage, and gas while the transaction is still live. -- **Go low or high level**: Whether you are checking invariants with the source in hand, or dropping into raw EVM to build a jump-oriented programming (JOP) chain from a contract's bytecode, sevm handles both. -- **See everything**: No source, no problem. sevm maps every public function and the flow through it, wired into the decompiled output, so you can see what storage and memory changed at each step. +- **Go low or high level**: Whether you are checking invariants with the source in hand, or dropping into raw EVM to build a jump-oriented programming (JOP) chain from a contract's bytecode, Chasm handles both. +- **See everything**: No source, no problem. Chasm maps every public function and the flow through it, wired into the decompiled output, so you can see what storage and memory changed at each step. - **Foundry compatible**: Use Foundry cheatcodes while debugging. - **Frontend-independent**: Drive the Rust engine through the bundled JSON-RPC process from an editor, web service, terminal UI, or any other client. @@ -40,7 +40,7 @@ ## Install -Clone the repository and install `sevm` as a global command: +Clone the repository and install `chasm` as a global command: ```bash git clone https://github.com/ChasmHQ/debugger && cd debugger @@ -51,62 +51,65 @@ pipx install uv uv tool install . ``` -Building from this checkout needs Python 3.10+, Rust 1.91+, and `git`. sevm downloads solc +Building from this checkout needs Python 3.10+, Rust 1.91+, and `git`. chasm downloads solc itself into `~/.solcx`, picking the build for this machine. x86-64 and arm64 Linux, Intel and Apple silicon macOS, and Windows are covered, and a solc already installed by Foundry (`~/.svm`) or sitting on `PATH` is used instead. Anywhere else, on musl systems like -Alpine, NixOS, or an architecture Solidity does not publish for, sevm falls back to -solc's WebAssembly build, which needs `node` on PATH (or named by `SEVM_NODE`). Failing -that, point sevm at a compiler you have: +Alpine, NixOS, or an architecture Solidity does not publish for, chasm falls back to +solc's WebAssembly build, which needs `node` on PATH (or named by `CHASM_NODE`). Failing +that, point chasm at a compiler you have: ```bash -sevm compile --solc-binary /usr/bin/solc examples/bank/src +chasm compile --solc-binary /usr/bin/solc examples/bank/src ``` -`SEVM_SOLC=/usr/bin/solc` does the same for every command. +`CHASM_SOLC=/usr/bin/solc` does the same for every command. ## Usage -sevm has the following subcommands: +Chasm has the following subcommands: - `run`: debug a Foundry test, or a web3.py driver script. -- `compile`: compile the contracts and report what sevm sees. -- `doctor`: report the compiler, runtimes and caches sevm found on this machine. +- `compile`: compile the contracts and report what chasm sees. +- `doctor`: report the compiler, runtimes and caches chasm found on this machine. For detailed information on each command and its options, run: ```bash -sevm -h -sevm -h +chasm -h +chasm -h ``` -By default `sevm run` opens the fullscreen interface in the screenshot above. The examples +By default `chasm run` opens the fullscreen interface in the screenshot above. The examples below use `--console`, the plain-text frontend, but the commands are identical in both. ### Debug a Foundry test -Point `sevm run` at a `.sol` test and it runs without an existing Foundry project, even -when the file imports forge-std or other libraries. sevm resolves the imports, clones what +Point `chasm run` at a `.sol` test and it runs without an existing Foundry project, even +when the file imports forge-std or other libraries. chasm resolves the imports, clones what is missing, writes the remappings, and breaks on the first line of the test body: ```bash -$ sevm run --console -y examples/standalone/Vault.t.sol -standalone test at examples/standalone; compiling ... -installing forge-std from https://github.com/foundry-rs/forge-std @ v1.16.2 +$ chasm run --console -y examples/standalone/Vault.t.sol +standalone test at /tmp/debugger2/examples/standalone; compiling ... +installing forge-std from https://github.com/foundry-rs/forge-std @ v1.17.0 installing @openzeppelin/contracts from https://github.com/OpenZeppelin/openzeppelin-contracts @ v5.7.0 wrote 2 remapping(s) to remappings.txt -wrote 25 artifact(s) to out/sevm +wrote 25 artifact(s) to out/chasm debugging 1 test(s): VaultTest.testDeal +Chasm - Solidity/EVM debugger. gdb commands; `help` for the list. Breakpoint 1, VaultTest.testDeal() at Vault.t.sol:11 11 vm.deal(alice, 3 ether); +pc 0x0c3d PUSH20 sp 3 gas 29,978,755 step 126 +(chasm) ``` Each test gets a fresh deploy, a `setUp()` and the test call, as forge isolates them. With -no filter, sevm debugs every test it discovers and `continue` runs to the next test body. +no filter, chasm debugs every test it discovers and `continue` runs to the next test body. `-m/--match` narrows to test functions matching a substring, `--match-contract` to a contract: ```bash -sevm run --console -m testDeposit examples/bank/test/Bank.t.sol +chasm run --console -m testDeposit examples/bank/test/Bank.t.sol ``` Inside a project with a `foundry.toml`, nothing is fetched. The project's `remappings.txt`, @@ -114,21 +117,23 @@ Inside a project with a `foundry.toml`, nothing is fetched. The project's `remap ### Debug a web3.py script -`sevm run` also takes a plain web3.py script. A script that constructs -`EthereumTesterProvider` runs unmodified: while the driver is active, sevm supplies its +`chasm run` also takes a plain web3.py script. A script that constructs +`EthereumTesterProvider` runs unmodified: while the driver is active, chasm supplies its REVM-backed provider, runs the script on a worker thread, and stops the first time execution enters code it recognises: ```bash -$ sevm run --console --contracts examples/bank/src examples/debug_bank.py -compiling examples/bank/src ... -cache hit (1 sources) +$ chasm run --console --contracts examples/bank/src examples/debug_bank.py +compiling /tmp/debugger/examples/bank/src ... +wrote 2 artifact(s) to out/chasm 2 contract(s): Bank, Callee -bank : 0xF2E246BB76DF876Cef8b38ae84130F4F55De395b -alice: 0x2da044dB7416b5210C46b2dCdd521DBff696F3aa +bank : 0x092cAc6DB1DE08aE9590C7528a5E050FEA367552 +alice: 0x72C7F68b160Fd5bF38A207097B3e8cFBC4C9E084 +Chasm - Solidity/EVM debugger. gdb commands; `help` for the list. Bank.deposit() at src/Bank.sol:51 51 function deposit() public payable { -(sevm) +pc 0x0379 JUMPDEST sp 1 gas 278,754 step 570 +(chasm) ``` Recognition is by bytecode, not by instrumentation, which is why an unmodified script @@ -136,17 +141,17 @@ works. Startup commands run before the prompt opens, so a session can land where it: ```bash -sevm run -x 'b _credit' -x c --contracts examples/bank/src examples/debug_bank.py +chasm run -x 'b _credit' -x c --contracts examples/bank/src examples/debug_bank.py ``` ### Connect another frontend -`sevm-engine` exposes the same Rust debugger core as JSON-RPC 2.0 over newline-delimited +`chasm-engine` exposes the same Rust debugger core as JSON-RPC 2.0 over newline-delimited standard input and output. A frontend can own presentation and process lifecycle without embedding Python or depending on the console or TUI: ```bash -sevm-engine < examples/headless-session.jsonl +chasm-engine < examples/headless-session.jsonl ``` The protocol supports persistent chains, transactions, stepping, breakpoints, snapshots, @@ -162,12 +167,12 @@ For a script that takes its calldata as an argument, that is the whole iterate l one session, without relaunching anything: ```bash -(sevm) b CALLDATACOPY -(sevm) c +(chasm) b CALLDATACOPY +(chasm) c Breakpoint 1, ... pc 0x015c CALLDATACOPY sp 1->7 gas 29,976,201 step 101 -(sevm) run 0x000040c3...your next attempt... -(sevm) c # breakpoint survived the restart +(chasm) run 0x000040c3...your next attempt... +(chasm) c # breakpoint survived the restart ``` Every stop ends with a one-line machine echo, giving pc, opcode, gas, step, and the stack @@ -179,9 +184,9 @@ will accept as one line, so an argument of the form `@path` is read from that fi instead, with whitespace stripped. It works at launch, at the prompt, and standalone: ```bash -sevm run ... script.py @C:/path/to/payload.hex # at launch -(sevm) run @C:/path/to/payload.hex # at the prompt -python script.py @payload.hex # without sevm at all +chasm run ... script.py @C:/path/to/payload.hex # at launch +(chasm) run @C:/path/to/payload.hex # at the prompt +python script.py @payload.hex # without chasm at all ``` ### Set breakpoints @@ -190,24 +195,24 @@ Break on a function, a source line, a raw program counter, or on every occurrenc opcode in any contract: ```bash -(sevm) b _credit # a function, by name or Contract.name -(sevm) b Bank.sol:46 # a source line, snapped to the next line with code -(sevm) b *0x108 # a raw program counter -(sevm) b SSTORE # every SSTORE, anywhere -(sevm) b Bank.sol:46 if amount > 1 ether +(chasm) b _credit # a function, by name or Contract.name +(chasm) b Bank.sol:46 # a source line, snapped to the next line with code +(chasm) b *0x108 # a raw program counter +(chasm) b SSTORE # every SSTORE, anywhere +(chasm) b Bank.sol:46 if amount > 1 ether ``` An opcode breakpoint is the way into code you have no line numbers for: ```bash -(sevm) b SSTORE +(chasm) b SSTORE Breakpoint 1 on every SSTORE -(sevm) c +(chasm) c Breakpoint 1, Bank._credit(address, uint256) at src/Bank.sol:46 46 balances[who] += amount - fee; ``` -sevm also stops on reverts by default, at the failing instruction, and decodes the reason +chasm also stops on reverts by default, at the failing instruction, and decodes the reason as `reverted: "..."`, `panic 0x11`, or a custom error with its arguments. ### Inspect the frame @@ -215,7 +220,7 @@ as `reverted: "..."`, `panic 0x11`, or a custom error with its arguments. `info registers` is the machine at a glance: ```bash -(sevm) info registers +(chasm) info registers pc 0x09b5 opcode PUSH0 (0x5f) gas 278,719 remaining / 217 used of 278,936 @@ -234,7 +239,7 @@ step 582 single slot number: ```bash -(sevm) info storage +(chasm) info storage Bank at 0xf2e246bb76df876cef8b38ae84130f4f55de395b slot 0+0 owner address = 0x7e5f4552091a69125d5dfcb7b8c2659029395bdf (cold) slot 0+20 feeBps uint96 = 25 (cold) @@ -242,14 +247,14 @@ Bank at 0xf2e246bb76df876cef8b38ae84130f4f55de395b slot 2+0 balances mapping(address => uint256) = (cold) slot 3+0 accounts mapping(address => struct Bank.Account) = (cold) slot 4+0 history uint256[] = [0 items] [] (cold) - slot 5+0 name string = "sevm-bank" (cold) + slot 5+0 name string = "chasm-bank" (cold) ``` `bt` walks the call stack. Internal Solidity calls get their own frames even though the EVM never changed depth: ```bash -(sevm) bt +(chasm) bt -> #0 Bank._credit(address, uint256) at src/Bank.sol:45 #1 Bank.deposit() at src/Bank.sol:52 #2 Bank at pc 0x380 @@ -259,7 +264,7 @@ never changed depth: line each instruction belongs to, and `x/NFU` examines memory in gdb syntax: ```bash -(sevm) l +(chasm) l src/Bank.sol . 40 function _fee(uint256 amount) internal view returns (uint256) { . 41 return (amount * feeBps) / 10000; @@ -272,13 +277,13 @@ src/Bank.sol . 48 history.push(amount); 49 } 50 -(sevm) disas +(chasm) disas 09b3 JUMP L51 09b4 JUMPDEST L44 => 09b5 PUSH0 L45 09b6 PUSH2 0x09be L45 09b9 DUP3 L45 -(sevm) x/32xb 0x80 +(chasm) x/32xb 0x80 0x0080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x0090: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 (memory is 96 bytes; the rest reads as zero) @@ -290,62 +295,62 @@ src/Bank.sol state snapshot, and throws the snapshot away, so it cannot disturb the run: ```bash -(sevm) p balances[who] + 100 ether +(chasm) p balances[who] + 100 ether $1 = 100000000000000000000 (100 ether) (uint256) -(sevm) p keccak256(abi.encode(who, amount)) +(chasm) p keccak256(abi.encode(who, amount)) $2 = 0x7a26c77763128d91079351eafa8c68f983a19e3262ef0fcc7ac5a257c2253c3a (bytes32) -(sevm) p type(uint256).max +(chasm) p type(uint256).max $3 = 115792089237316195423570985008687907853269984665640564039457584007913129639935 (uint256) ``` Because it *is* Solidity, `internal` and `private` functions are callable too: ```bash -(sevm) p _fee(1 ether) +(chasm) p _fee(1 ether) $4 = 2500000000000000 (0.002500 ether) (uint256) ``` `p` discards its side effects. `call` keeps them: ```bash -(sevm) call setNickname("hodler") +(chasm) call setNickname("hodler") done (gas 27599) -(sevm) p accounts[msg.sender].nickname +(chasm) p accounts[msg.sender].nickname $5 = "hodler" (string memory) -(sevm) call history.push(7) +(chasm) call history.push(7) done (gas 44484) -(sevm) p history.length +(chasm) p history.length $6 = 1 (uint256) ``` `display` re-evaluates an expression at every stop, and `ptype` reports a type: ```bash -(sevm) display fee +(chasm) display fee 1: fee = -(sevm) n +(chasm) n Bank._credit(address, uint256) at src/Bank.sol:46 46 balances[who] += amount - fee; 1: fee = 5000000000000000 (0.005000 ether) -(sevm) ptype balances[who] +(chasm) ptype balances[who] type = uint256 ``` ### Read local variables -solc emits no location for locals, so sevm reconstructs them from the AST and the run's +solc emits no location for locals, so chasm reconstructs them from the AST and the run's stack heights. `info locals` names, types and decodes them: ```bash -(sevm) info locals +(chasm) info locals who address = 0x2da044db7416b5210c46b2dcdd521dbff696f3aa (param) amount uint256 = 2000000000000000000 (2 ether) (param) fee uint256 = this instruction allocates it; step once to see it -(sevm) n +(chasm) n Bank._credit(address, uint256) at src/Bank.sol:46 46 balances[who] += amount - fee; -(sevm) p amount - fee +(chasm) p amount - fee $1 = 1995000000000000000 (1.995000 ether) (uint256) ``` @@ -356,9 +361,9 @@ A local that cannot be read says why rather than printing a wrong number. A watchpoint breaks when a value changes, and reports the old and new values: ```bash -(sevm) watch totalDeposits +(chasm) watch totalDeposits Watchpoint 2: totalDeposits (slot 0x1 of 0xf2e2..395b) -(sevm) c +(chasm) c Watchpoint 2 totalDeposits: 0xde0b6b3a7640000 -> 0x299060a1be4b8000 Bank._credit(address, uint256) at src/Bank.sol:47 47 totalDeposits += amount - fee; @@ -374,11 +379,11 @@ correctly. The write lands in the transaction that is still running, and it chan outcome: ```bash -(sevm) set var fee = 1 ether +(chasm) set var fee = 1 ether fee = 1000000000000000000 (1 ether) -(sevm) set var balances[who] = 5 ether +(chasm) set var balances[who] = 5 ether balances[who] = 5000000000000000000 (5 ether) -(sevm) c +(chasm) c total deposits: 2000000000000000000 alice balance : 6000000000000000000 ``` @@ -389,10 +394,10 @@ Without those two writes the same script prints `2995000000000000000` and The lower-level writes are there too: ```bash -(sevm) set $stack[0] = 0xc0ffee # rewrite an operand before its opcode consumes it -(sevm) set $mem[0x80] = 1 -(sevm) set $storage[0] = 0xdead -(sevm) jump 0x108 # move the program counter, JUMPDESTs only +(chasm) set $stack[0] = 0xc0ffee # rewrite an operand before its opcode consumes it +(chasm) set $mem[0x80] = 1 +(chasm) set $storage[0] = 0xdead +(chasm) jump 0x108 # move the program counter, JUMPDESTs only ``` ### Force an out-of-gas @@ -400,9 +405,9 @@ The lower-level writes are there too: Set the gas counter to whatever you need and let the transaction run into the wall: ```bash -(sevm) set $gas = 100 +(chasm) set $gas = 100 $gas = 100 -(sevm) c +(chasm) c Stopped on error: OutOfGas: out of gas; `set $gas = N` then `c` retries this instruction Bank._fee(uint256) at src/Bank.sol:41 41 return (amount * feeBps) / 10000; @@ -414,8 +419,8 @@ meter and continue, and the failed instruction is retried on its original stack. transaction then runs as if it had been sent with the larger limit: ```bash -(sevm) set $gas = 5000000 -(sevm) c +(chasm) set $gas = 5000000 +(chasm) c ``` The failure is real, not simulated. The script's own output shows the deposit never @@ -431,7 +436,7 @@ alice balance : 0 `info gas` breaks the spend down by source line and by opcode: ```bash -(sevm) info gas +(chasm) info gas limit 278,936 used 28,864 remaining 250,072 @@ -459,19 +464,19 @@ A failed forge-std assertion stops the debugger where it broke, with the compari decoded, and `up` walks back to your test: ```bash -$ sevm run --console -y -m testBalanceIgnoresFee examples/bank/test/FailingAssertion.t.sol +$ chasm run --console -y -m testBalanceIgnoresFee examples/bank/test/FailingAssertion.t.sol debugging 1 test(s): FailingAssertionTest.testBalanceIgnoresFee Breakpoint 1, FailingAssertionTest.testBalanceIgnoresFee() at test/FailingAssertion.t.sol:18 18 vm.prank(alice); -(sevm) c +(chasm) c Stopped on error: reverted: "assertion failed: 997500000000000000 != 1000000000000000000" StdAssertions.assertEq(uint256, uint256) at lib/forge-std/src/StdAssertions.sol:121 121 vm.assertEq(left, right); -(sevm) up +(chasm) up #1 FailingAssertionTest.testBalanceIgnoresFee() at test/FailingAssertion.t.sol:20 ``` -This works because sevm implements the `vm.assert*` family as real comparisons, all 116 +This works because chasm implements the `vm.assert*` family as real comparisons, all 116 overloads that forge-std's own `assertEq` and friends call into. ### Use Foundry cheatcodes @@ -480,15 +485,15 @@ Cheatcodes run against live REVM state, both from inside the test and typed at t against the frame you are stopped in: ```bash -(sevm) vm.warp(12345) +(chasm) vm.warp(12345) vm.warp -> ok -(sevm) p block.timestamp +(chasm) p block.timestamp $1 = 12345 (uint256) -(sevm) vm.deal(alice, 5 ether) +(chasm) vm.deal(alice, 5 ether) vm.deal -> ok -(sevm) p alice.balance +(chasm) p alice.balance $2 = 5000000000000000000 (5 ether) (uint256) -(sevm) vm.prank(address(0xcafe)) +(chasm) vm.prank(address(0xcafe)) vm.prank -> ok ``` @@ -498,7 +503,7 @@ address the way Solidity's own `address(0xcafe)` does. When nothing fits, the er the argument and keeps solc's reason: ```bash -(sevm) vm.prank(alcie) +(chasm) vm.prank(alcie) error: vm.prank: argument 1 (alcie) is not a valid address (Undeclared identifier. Did you mean "alice"? (in `alcie`)) ``` @@ -516,11 +521,11 @@ Type a Yul builtin and it runs on the paused frame through REVM's opcode implementations. This is the low-level twin of `set var`: ```bash -(sevm) mload(0x40) +(chasm) mload(0x40) mload(0x40) -> $1 = 0x80 (128) (gas 12) -(sevm) sstore(3, add(sload(3), 1)) +(chasm) sstore(3, add(sload(3), 1)) sstore(3, add(sload(3), 1)) -> ok (gas 22,103) -(sevm) asm mstore(0x80, 1); mstore8(0xa0, 0x61); mload(0x80) +(chasm) asm mstore(0x80, 1); mstore8(0xa0, 0x61); mload(0x80) mstore(0x80, 1) -> ok (gas 18) mstore8(0xa0, 0x61) -> ok (gas 21) mload(0x80) -> $2 = 0x1 (1) (gas 18) @@ -531,7 +536,7 @@ transaction that succeeds into one that runs out of gas. Control-flow builtins a frame terminators are refused, with the reason: ```bash -(sevm) jump(0x10) +(chasm) jump(0x10) error: `jump`: Yul has no `jump`; use the debugger's `jump 0xPC` or `set $pc = 0xPC` ``` @@ -541,13 +546,13 @@ Convenience variables bypass solc, so they work on contracts you have no source they still mix into a Solidity expression: ```bash -(sevm) p $pc +(chasm) p $pc $1 = 2497 (uint256) -(sevm) p $storage[1] + 1 ether +(chasm) p $storage[1] + 1 ether $2 = 2000000000000000000 (2 ether) (uint256) -(sevm) p $stack[0] +(chasm) p $stack[0] $3 = 5000000000000000 (0.005000 ether) (uint256) -(sevm) x/32xb $mem[0x40] +(chasm) x/32xb $mem[0x40] 0x0080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x0090: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ``` @@ -559,9 +564,9 @@ Selectors come out of the bytecode rather than the ABI, so a 4-byte value from a be traced to the pc it routes to even when nothing here declares it: ```bash -(sevm) sig transfer(address,uint256) +(chasm) sig transfer(address,uint256) transfer(address,uint256) 0xa9059cbb (hashed here; not in this ABI) -(sevm) info address 0xa9059cbb +(chasm) info address 0xa9059cbb selector 0xa9059cbb the dispatcher never tests this selector: a call carrying it lands in the fallback ``` @@ -576,12 +581,12 @@ implementation's JUMPDEST behind it, which is the pc every caller converges on. hit includes instruction alignment and the nearest preceding JUMPDEST: ```bash -(sevm) find 5b +(chasm) find 5b ``` ### Debug from an AI client (MCP) -`sevm mcp` serves the debugger over the Model Context Protocol (stdio). It provides +`chasm mcp` serves the debugger over the Model Context Protocol (stdio). It provides windowed reads with explicit truncation, a stop report after every navigation, and a diff of what the last step changed. Operand provenance (`why N`) traces stack values through recorded opcodes. The server also checks compiled runtime bytecode against a @@ -590,11 +595,11 @@ and client configuration. ### Compile a project -`sevm compile` runs the same build the debugger does and reports what it produced, which is +`chasm compile` runs the same build the debugger does and reports what it produced, which is the fastest way to check whether a contract will be debuggable at all: ```bash -$ sevm compile examples/bank/src +$ chasm compile examples/bank/src cache hit (1 sources) solc 0.8.36, optimizer off src/Bank.sol id=0 lines=99 @@ -606,31 +611,31 @@ solc 0.8.36, optimizer off compilation unit, so an edit recompiles the file and its importers rather than the project: ```bash -$ cd examples/bank && time sevm compile . -wrote 27 artifact(s) to out/sevm +$ cd examples/bank && time chasm compile . +wrote 27 artifact(s) to out/chasm real 1.95 -$ time sevm compile . +$ time chasm compile . cache hit (22 sources) real 0.53 -$ time sevm compile . # after editing one test +$ time chasm compile . # after editing one test recompiled 1 of 22 sources real 1.02 ``` -Artifacts land in `out/sevm/` in forge's layout, and the cache in `cache/sevm/`. Both are +Artifacts land in `out/chasm/` in forge's layout, and the cache in `cache/chasm/`. Both are cleared by `forge clean`. A directory with no `foundry.toml` gets nothing written into it. ### Check the environment -`sevm doctor` says which compiler this machine will use, where it came from, and what is +`chasm doctor` says which compiler this machine will use, where it came from, and what is missing. It downloads nothing: a compiler that is not installed yet is reported as the URL it would be fetched from: ```bash -$ sevm doctor - sevm 0.1.0 +$ chasm doctor + chasm 0.1.0 python 3.12.14 (CPython) at /home/fev/Projects/debugger/.venv/bin/python3 platform linux-arm64 (Linux aarch64, glibc 2.43) git git version 2.55.0 @@ -639,13 +644,13 @@ $ sevm doctor available 68 releases, 0.5.0 .. 0.8.36 wasm solc /home/fev/.local/share/mise/installs/node/latest/bin/node v26.8.1 binaries /home/fev/.solcx - cache /home/fev/.cache/sevm + cache /home/fev/.cache/chasm ``` -Anything that would stop sevm working is marked `!` and makes the command exit non-zero, -so it doubles as a CI check. `platform` is the build sevm downloads for, and `available` +Anything that would stop chasm working is marked `!` and makes the command exit non-zero, +so it doubles as a CI check. `platform` is the build chasm downloads for, and `available` is what that platform publishes. Where nothing is published, `wasm solc` is what will run -instead, and an override from `SEVM_SOLC`, `SEVM_NODE` or `SOLCX_BINARY_PATH` is listed on +instead, and an override from `CHASM_SOLC`, `CHASM_NODE` or `SOLCX_BINARY_PATH` is listed on an `overrides` line so a stale environment variable cannot hide. ## Reference @@ -665,7 +670,7 @@ Set up an editable environment: ```bash uv sync -uv run sevm --help +uv run chasm --help ``` Format, lint, type-check, and build: @@ -683,7 +688,7 @@ The suite runs under pytest and lives in `tests/`: ```bash uv run pytest -q -SEVM_NETWORK_TESTS=1 uv run pytest -q -m network +CHASM_NETWORK_TESTS=1 uv run pytest -q -m network ``` ## License diff --git a/assets/screenshot.png b/assets/screenshot.png index 87c0c5e..6d4a138 100644 Binary files a/assets/screenshot.png and b/assets/screenshot.png differ diff --git a/docs/assembly.md b/docs/assembly.md index a066585..e1cd95d 100644 --- a/docs/assembly.md +++ b/docs/assembly.md @@ -8,17 +8,17 @@ that is actually running. [back to README](../README.md) ```bash -(sevm) mload(0x40) +(chasm) mload(0x40) mload(0x40) -> $1 = 0x80 (128) (gas 12) -(sevm) mstore(0x80, 0xdeadbeef) +(chasm) mstore(0x80, 0xdeadbeef) mstore(0x80, 0xdeadbeef) -> ok (gas 18) -(sevm) sstore(3, add(sload(3), 1)) +(chasm) sstore(3, add(sload(3), 1)) sstore(3, add(sload(3), 1)) -> ok (gas 22,103) -(sevm) asm mstore(0x80, 1); mstore8(0xa0, 0x61); mload(0x80) +(chasm) asm mstore(0x80, 1); mstore8(0xa0, 0x61); mload(0x80) mstore(0x80, 1) -> ok (gas 18) mstore8(0xa0, 0x61) -> ok (gas 21) mload(0x80) -> $2 = 0x1 (1) (gas 18) -(sevm) keccak256(0x80, 32) +(chasm) keccak256(0x80, 32) keccak256(0x80, 32) -> $3 = 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6 (gas 51) ``` @@ -41,7 +41,7 @@ literal (`"hi"`, right-padded as Yul pads it), a nested call, or any convenience Refused, with the reason: ```bash -(sevm) jump(0x10) +(chasm) jump(0x10) error: `jump`: Yul has no `jump`; use the debugger's `jump 0xPC` or `set $pc = 0xPC` ``` diff --git a/docs/commands.md b/docs/commands.md index fce43c1..03d2958 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -19,7 +19,7 @@ expressions are Solidity and the machine underneath is the EVM. | `u` / `until LOC` | run to a line or `*PC` | `step` and `next` understand *internal* Solidity calls. Those compile to `JUMP`, so EVM -depth never changes, and sevm tracks them through the source map's `i` and `o` jump +depth never changes, and chasm tracks them through the source map's `i` and `o` jump markers. Every stop ends with a one-line machine echo, so opcode-level stepping always shows what @@ -27,7 +27,7 @@ the last step did to the machine — the stack height as old->new when it change reads `sp 13->12`), plus pc, opcode, gas remaining and step count: ```bash -(sevm) si +(chasm) si pc 0x0077 POP sp 13->12 gas 2,843,413 step 5 ``` @@ -38,7 +38,7 @@ tester chain — a genuine clean slate for iterating on calldata. An argument of `@path` (at launch, or to `run`) is replaced by that file's whitespace-stripped contents: payload hex runs to thousands of characters, past what a Windows console or command line will carry. The same `@file` syntax works at launch -(`sevm run ... script.py @payload.hex`), at the prompt (`run @payload.hex`) and anywhere +(`chasm run ... script.py @payload.hex`), at the prompt (`run @payload.hex`) and anywhere else the script's arguments are set. ## Breakpoints @@ -58,7 +58,7 @@ else the script's arguments are set. Watchpoints work on state variables, on mapping elements (`watch balances[msg.sender]`), and on memory (`watch *0x80`). -sevm also stops on reverts by default, at the failing instruction, and decodes the reason +chasm also stops on reverts by default, at the failing instruction, and decodes the reason as `reverted: "..."`, `panic 0x11`, or a custom error with its arguments. ## Inspection @@ -86,7 +86,7 @@ as `reverted: "..."`, `panic 0x11`, or a custom error with its arguments. `info storage` decodes the whole layout, packed slots included: ```bash -(sevm) info storage +(chasm) info storage Bank at 0x4f9da333dcf4e5a53772791b95c161b2fc041859 slot 0+0 owner address = 0xf2e246bb...5b (cold) slot 0+20 feeBps uint96 = 25 (cold) @@ -105,22 +105,22 @@ three, and the implementation's JUMPDEST is the pc every caller converges on, so there catches a proxy, a raw-calldata call and an internal call alike: ```bash -(sevm) sig balances +(chasm) sig balances balances(address) 0x27e235e3 -(sevm) info address balances +(chasm) info address balances Bank.balances(address) selector 0x27e235e3 dispatch 0x0096 where the dispatcher compares it wrapper 0x0167 the external entry: guards, then decodes calldata internal 0x052f src/Bank.sol:17 break there with `b *0x52f` -(sevm) b *0x52f +(chasm) b *0x52f Breakpoint 4 at pc 0x52f -(sevm) c +(chasm) c Breakpoint 4, Bank at src/Bank.sol:17 17 mapping(address => uint256) public balances; // slot 2 pc 0x052f JUMPDEST sp 3 gas 29,400,111 step 2027 -(sevm) bt +(chasm) bt -> #0 at src/Bank.sol:17 #1 Bank at pc 0x18c #2 BankTest.testDeposit() at test/Bank.t.sol:19 @@ -135,9 +135,9 @@ Neither needs an ABI. A signature this contract does not declare is hashed, as ` does, and the selector is looked for in the bytecode anyway: ```bash -(sevm) sig transfer(address,uint256) +(chasm) sig transfer(address,uint256) transfer(address,uint256) 0xa9059cbb (hashed here; not in this ABI) -(sevm) info address 0xa9059cbb +(chasm) info address 0xa9059cbb selector 0xa9059cbb the dispatcher never tests this selector: a call carrying it lands in the fallback ``` @@ -169,5 +169,5 @@ They bypass solc, so they work on contracts with no source and still mix into a expression: ```bash -(sevm) p $storage[1] + 1 ether +(chasm) p $storage[1] + 1 ether ``` diff --git a/docs/expressions.md b/docs/expressions.md index fa34360..81187a9 100644 --- a/docs/expressions.md +++ b/docs/expressions.md @@ -9,13 +9,13 @@ same thing and keeps the effects. ## Evaluating Solidity ```bash -(sevm) p balances[who] + 100 ether +(chasm) p balances[who] + 100 ether $1 = 100000000000000000000 (100 ether) (uint256) -(sevm) p keccak256(abi.encode(who, amount)) +(chasm) p keccak256(abi.encode(who, amount)) $2 = 0x6515e1c6183591491b775874fa6d6c2f905cfd817ab77e8ece0958bcc578bf7b (bytes32) -(sevm) p type(uint256).max +(chasm) p type(uint256).max $3 = 115792089237316195423570985008687907853269984665640564039457584007913129639935 (uint256) -(sevm) p accounts[msg.sender].nickname +(chasm) p accounts[msg.sender].nickname $4 = "hodler" (string memory) ``` @@ -27,23 +27,23 @@ expression, so a `display` costs one compile. `msg.*` reads the frame you are stopped in, calldata included: ```bash -(sevm) p msg.sig +(chasm) p msg.sig $5 = 0xd0e30db0 (bytes4) -(sevm) p msg.data.length +(chasm) p msg.data.length $6 = 4 (uint256) ``` Calldata slices decode as they would in Solidity, so an argument can be pulled out of a -frame whose signature sevm has no source for: +frame whose signature chasm has no source for: ```bash -(sevm) p abi.decode(msg.data[36:], (uint256)) +(chasm) p abi.decode(msg.data[36:], (uint256)) ``` A mapping or a struct has to be indexed rather than printed whole: ```bash -(sevm) ptype balances +(chasm) ptype balances error: EvalError: cannot display a whole mapping; index it with a key ``` @@ -53,22 +53,22 @@ error: EvalError: cannot display a whole mapping; index it with a key takes expressions over them: ```bash -(sevm) info locals +(chasm) info locals who address = 0x0278bdd7808aa64dc93c361ae55fc52cf1a918cf (param) amount uint256 = 2000000000000000000 (2 ether) (param) fee uint256 = 5000000000000000 (0.005000 ether) -(sevm) p amount - fee +(chasm) p amount - fee $1 = 1995000000000000000 (1.995000 ether) (uint256) -(sevm) set var fee = 1 ether +(chasm) set var fee = 1 ether ``` -solc emits no location for locals, so sevm reconstructs it from the AST and the run's stack +solc emits no location for locals, so chasm reconstructs it from the AST and the run's stack heights, the way Truffle and Remix do. A value shows only when the frame was observed from entry, when the slot is still below the top of the stack, and when the current instruction is inside the declaration's scope. Otherwise you get `` with the reason: ```bash -(sevm) info locals +(chasm) info locals fee uint256 = this instruction allocates it; step once to see it ``` diff --git a/docs/foundry.md b/docs/foundry.md index ccc9e2e..6e6b4ef 100644 --- a/docs/foundry.md +++ b/docs/foundry.md @@ -1,16 +1,16 @@ # Foundry projects, libraries and the build cache -sevm reads a Foundry project the way `forge` does, and writes back into it in a way that +Chasm reads a Foundry project the way `forge` does, and writes back into it in a way that cannot disturb `forge`. This page covers the parts the README summarises. [back to README](../README.md) -`sevm run` dispatches on the extension. A `.py` argument is a web3 driver, a `.sol` +`chasm run` dispatches on the extension. A `.py` argument is a web3 driver, a `.sol` argument is a Foundry test. ```bash -sevm run test/Counter.t.sol # fullscreen TUI, opens in the first test -sevm run --console -m testDeposit examples/bank/test/Bank.t.sol # one test by name +chasm run test/Counter.t.sol # fullscreen TUI, opens in the first test +chasm run --console -m testDeposit examples/bank/test/Bank.t.sol # one test by name ``` Each test gets a fresh deploy, a `setUp()` and the test call, as forge isolates them, and @@ -36,12 +36,12 @@ contract VaultTest is Test { ``` ```bash -$ sevm run --console -y examples/standalone/Vault.t.sol +$ chasm run --console -y examples/standalone/Vault.t.sol standalone test at examples/standalone; compiling ... installing forge-std from https://github.com/foundry-rs/forge-std @ v1.16.2 installing @openzeppelin/contracts from https://github.com/OpenZeppelin/openzeppelin-contracts @ v5.7.0 wrote 2 remapping(s) to remappings.txt -wrote 25 artifact(s) to out/sevm +wrote 25 artifact(s) to out/chasm debugging 1 test(s): VaultTest.testDeal Breakpoint 1, VaultTest.testDeal() at Vault.t.sol:11 11 vm.deal(alice, 3 ether); @@ -55,30 +55,30 @@ examples/standalone ├── foundry.toml [profile.default] with libs = ["lib"], no src/test ├── remappings.txt forge-std/=lib/forge-std/src/ │ @openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ -├── cache/sevm the compilation unit, keyed by solc version, settings, sources -├── out/sevm one JSON per contract, in forge's layout +├── cache/chasm the compilation unit, keyed by solc version, settings, sources +├── out/chasm one JSON per contract, in forge's layout └── lib ├── forge-std v1.16.2 └── openzeppelin-contracts v5.7.0 ``` -Drop `-y` and sevm asks once before writing anything: +Drop `-y` and chasm asks once before writing anything: ```bash -$ sevm run --console examples/standalone/Vault.t.sol -sevm will: +$ chasm run --console examples/standalone/Vault.t.sol +chasm will: - create examples/standalone/foundry.toml - install forge-std, @openzeppelin/contracts into examples/standalone/lib [y/N] ``` -Answer `n` and sevm writes nothing, compiling against what is already there. A +Answer `n` and chasm writes nothing, compiling against what is already there. A non-interactive run declines by itself. `--no-install` skips the prompt and refuses the same way, with the manual recipe: ```bash -$ sevm run --console --no-install examples/standalone/Vault.t.sol -compile failed: unresolved import 'forge-std/Test.sol' in Vault.t.sol, and sevm was told +$ chasm run --console --no-install examples/standalone/Vault.t.sol +compile failed: unresolved import 'forge-std/Test.sol' in Vault.t.sol, and chasm was told not to install it. Run again with -y to let it, or install it yourself: forge install / echo 'forge-std/=lib//src/' >> remappings.txt @@ -87,7 +87,7 @@ not to install it. Run again with -y to let it, or install it yourself: ## Inside a Foundry project ```bash -$ sevm run --console examples/bank/test/Bank.t.sol +$ chasm run --console examples/bank/test/Bank.t.sol foundry project at examples/bank; compiling ... debugging 3 test(s): BankTest.testDeposit, BankTest.testFeeIsTakenOnDeposit, FailingAssertionTest.testBalanceIgnoresFee @@ -101,13 +101,13 @@ has none. ## How an import is resolved -sevm looks up the prefix in its own table (`forge-std`, `ds-test`, `solmate`, `solady`, +chasm looks up the prefix in its own table (`forge-std`, `ds-test`, `solmate`, `solady`, openzeppelin), then in npm's registry metadata for anything else, and clones the repository it finds at the newest release tag. Prereleases are skipped. The remapping comes from where the imported file actually landed in the clone, so `src/`, `contracts/` and flat layouts all work. -A clone is a pin. sevm never updates it. To move a version, delete `lib/` or run +A clone is a pin. chasm never updates it. To move a version, delete `lib/` or run `forge install /@` yourself. git is required, the `forge` binary is not, and only the first run for a given library needs the network. @@ -116,43 +116,43 @@ only the first run for a given library needs the network. The first run compiles. The next one does not. ```bash -$ time sevm compile . -wrote 27 artifact(s) to out/sevm +$ time chasm compile . +wrote 27 artifact(s) to out/chasm solc 0.8.36, optimizer off real 1.95 -$ time sevm compile . +$ time chasm compile . cache hit (22 sources) real 0.53 $ $EDITOR test/Bank.t.sol -$ time sevm compile . +$ time chasm compile . recompiled 1 of 22 sources -wrote 27 artifact(s) to out/sevm +wrote 27 artifact(s) to out/chasm real 1.02 ``` A build leaves the same two directories forge does: ``` -cache/sevm/ the compilation unit, keyed by solc version, settings and source content -out/sevm/ one JSON per contract, in forge's layout: out/sevm/Token.sol/Token.json +cache/chasm/ the compilation unit, keyed by solc version, settings and source content +out/chasm/ one JSON per contract, in forge's layout: out/chasm/Token.sol/Token.json ``` An edit invalidates the file you changed and everything that imports it. The rest is reused, so solc only has to emit the parts that moved. `forge clean` clears both of those along with forge's own. -Artifacts are nested under `out/sevm/` rather than written straight into `out/`, because -sevm compiles with the optimizer off. Overwriting `out/Token.sol/Token.json` would leave -forge's cache calling it fresh, and the next `forge test` would run sevm's build. They +Artifacts are nested under `out/chasm/` rather than written straight into `out/`, because +chasm compiles with the optimizer off. Overwriting `out/Token.sol/Token.json` would leave +forge's cache calling it fresh, and the next `forge test` would run chasm's build. They carry `abi`, `bytecode`, `deployedBytecode`, `methodIdentifiers`, `storageLayout` and the -source id, with forge's field names. `metadata` is the one thing missing, as sevm never +source id, with forge's field names. `metadata` is the one thing missing, as chasm never asks solc for it. A directory with no `foundry.toml` gets nothing written into it. Its cache lives under -`~/.cache/sevm/` and no artifacts are written at all. `--force` recompiles and rewrites the -entry, `--no-cache` (or `SEVM_NO_CACHE=1`) writes nothing anywhere. +`~/.cache/chasm/` and no artifacts are written at all. `--force` recompiles and rewrites the +entry, `--no-cache` (or `CHASM_NO_CACHE=1`) writes nothing anywhere. ## Cheatcodes @@ -177,25 +177,25 @@ Implemented: - `assume`, and the full `vm.assert*` family (`assertEq`, `assertGt`, `assertApproxEqRel`, the `*Decimal` forms, 116 overloads) that forge-std's own `assertEq` calls into. - gas-metering knobs (`pauseGasMetering resumeGasMetering resetGasMetering`) and `skip` are - accepted as no-ops, since sevm meters gas itself and produces no gas snapshots. + accepted as no-ops, since chasm meters gas itself and produces no gas snapshots. A failed assertion stops the debugger where it broke, with the comparison: ```bash -(sevm) c +(chasm) c Stopped on error: reverted: "assertion failed: 997500000000000000 != 1000000000000000000" StdAssertions.assertEq(uint256, uint256) at lib/forge-std/src/StdAssertions.sol:121 121 vm.assertEq(left, right); -(sevm) up +(chasm) up #1 FailingAssertionTest.testBalanceIgnoresFee() at test/FailingAssertion.t.sol:20 ``` Fire one at the prompt against the frame you are stopped in: ```bash -(sevm) vm.warp(12345) +(chasm) vm.warp(12345) vm.warp -> ok -(sevm) p block.timestamp +(chasm) p block.timestamp $1 = 12345 (uint256) ``` @@ -204,13 +204,13 @@ as one, and otherwise a Solidity expression evaluated against the paused frame, `p` evaluates one: ```bash -(sevm) vm.deal(alice, 5 ether) +(chasm) vm.deal(alice, 5 ether) vm.deal -> ok -(sevm) p alice.balance +(chasm) p alice.balance $2 = 5000000000000000000 (5 ether) (uint256) -(sevm) vm.label(bank.owner(), "owner") +(chasm) vm.label(bank.owner(), "owner") vm.label -> ok -(sevm) vm.getLabel(bank.owner()) +(chasm) vm.getLabel(bank.owner()) vm.getLabel -> owner ``` @@ -223,7 +223,7 @@ parameter. If that does not fit either, the error names the argument and carries reason: ```bash -(sevm) vm.prank(alcie) +(chasm) vm.prank(alcie) error: vm.prank: argument 1 (alcie) is not a valid address (Undeclared identifier. Did you mean "alice"? (in `alcie`)) ``` diff --git a/docs/headless.md b/docs/headless.md index c25adda..b33d5b7 100644 --- a/docs/headless.md +++ b/docs/headless.md @@ -1,6 +1,6 @@ # Headless REVM protocol -The REVM engine can run without the sevm console or fullscreen interface. +The REVM engine can run without the Chasm console or fullscreen interface. Any frontend that can launch a process and exchange newline-delimited JSON can control the same live debugger session. This includes editor extensions, browser backends, terminal interfaces, and clients written in languages other than Python. @@ -16,8 +16,8 @@ complete request stream that pauses before `SSTORE`, changes its value operand f to `9`, resumes, and reads the committed storage from the finish event: ```bash -$ uv run sevm-engine < examples/headless-session.jsonl -{"id":1,"jsonrpc":"2.0","result":{"methods":["hello","open","start","transact","wait_event","snapshot","set_breakpoints","set_stack","write_memory","set_gas","set_pc","step","read_storage","write_storage","state","evaluate","evaluate_call","execute_opcode","set_prank","respond_host","resume","close","shutdown"],"protocol":"sevm-debugger/1","transport":"jsonl-stdio"}} +$ uv run chasm-engine < examples/headless-session.jsonl +{"id":1,"jsonrpc":"2.0","result":{"methods":["hello","open","start","transact","wait_event","snapshot","set_breakpoints","set_stack","write_memory","set_gas","set_pc","step","read_storage","write_storage","state","evaluate","evaluate_call","execute_opcode","set_prank","respond_host","resume","close","shutdown"],"protocol":"chasm-debugger/1","transport":"jsonl-stdio"}} {"id":2,"jsonrpc":"2.0","result":{"started":true}} {"id":3,"jsonrpc":"2.0","result":{"snapshot":{"address":"0x1000000000000000000000000000000000000001","calldata":"0x","caller":"0x2000000000000000000000000000000000000002","code_address":"0x1000000000000000000000000000000000000001","depth":0,"frames":[{"address":"0x1000000000000000000000000000000000000001","calldata":"0x","caller":"0x2000000000000000000000000000000000000002","code":"0x600160005500","code_address":"0x1000000000000000000000000000000000000001","depth":0,"gas_limit":79000,"gas_remaining":78994,"is_static":false,"kind":"call","opcode":85,"pc":4,"value":"0x0"}],"gas_limit":79000,"gas_refund":0,"gas_remaining":78994,"gas_used":6,"is_static":false,"memory":"0x","memory_size":0,"mnemonic":"SSTORE","opcode":85,"origin":"0x2000000000000000000000000000000000000002","pc":4,"reason":"breakpoint","stack":["0x0","0x1"],"step":3,"value":"0x0"},"type":"paused"}} {"id":4,"jsonrpc":"2.0","result":"0x9"} @@ -26,12 +26,12 @@ $ uv run sevm-engine < examples/headless-session.jsonl {"id":7,"jsonrpc":"2.0","result":null} ``` -An installed package exposes the same process as `sevm-engine`. A frontend launches that +An installed package exposes the same process as `chasm-engine`. A frontend launches that command and keeps its standard input and standard output pipes open. A Rust application -can instead depend on `sevm-revm-headless` and call `serve` with any buffered reader and +can instead depend on `chasm-revm-headless` and call `serve` with any buffered reader and writer. -Call `hello` first and require `sevm-debugger/1`. A client should stop if it receives a +Call `hello` first and require `chasm-debugger/1`. A client should stop if it receives a protocol version it does not support. ## Requests diff --git a/docs/mcp.md b/docs/mcp.md index 8fda76f..f6b2b17 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1,6 +1,6 @@ -# sevm MCP server for AI clients +# Chasm MCP server for AI clients -`sevm mcp` runs the REVM debugger as a [Model Context Protocol](https://modelcontextprotocol.io) +`chasm mcp` runs the REVM debugger as a [Model Context Protocol](https://modelcontextprotocol.io) server over stdio. Its tools return compact JSON for clients that inspect and step through transactions. @@ -11,7 +11,7 @@ through transactions. | A human... | An AI client... | So the tools... | |---|---|---| | scrolls the memory pane | gets one fixed response | every windowed read takes `offset`/`words` and returns `{items, total, truncated}`; narrow the window for more | -| watches panes change live | only sees tool results | `sevm_diff_since_last_stop` reports stack pushes/pops, changed memory ranges and gas after each step | +| watches panes change live | only sees tool results | `chasm_diff_since_last_stop` reports stack pushes/pops, changed memory ranges and gas after each step | | knows where they are | must be told, every time | every navigation tool returns the same stop report (pc, opcode, gas, sp, location, stack top, stack delta) | | reads syntax highlighting | pays per token | output is plain JSON: full-width hex words, decimal counts, no markup | | restarts on a typo | cannot recover silently | failures return `{"error": "..."}` with the fix in the message | @@ -19,9 +19,9 @@ through transactions. ## Running it ```bash -sevm mcp -uv run sevm mcp -sevm mcp --timeout 300 +chasm mcp +uv run chasm mcp +chasm mcp --timeout 300 ``` Any MCP client that speaks stdio can connect. Generic configuration: @@ -29,8 +29,8 @@ Any MCP client that speaks stdio can connect. Generic configuration: ```json { "mcpServers": { - "sevm": { - "command": "sevm", + "chasm": { + "command": "chasm", "args": ["mcp"] } } @@ -45,46 +45,46 @@ One debug session is active at a time. Starting a new target replaces it. | Tool | Meaning | |---|---| -| `sevm_start_session` | compile and start a web3.py driver script or a `.t.sol` Foundry test. Extra `args` go to scripts, and `match`/`match_contract` select tests. Returns the first stop report | -| `sevm_stop_session` | dispose the session (chain state is discarded) | -| `sevm_restart_session` | re-run from scratch: fresh chain, breakpoints kept, optional new script args | +| `chasm_start_session` | compile and start a web3.py driver script or a `.t.sol` Foundry test. Extra `args` go to scripts, and `match`/`match_contract` select tests. Returns the first stop report | +| `chasm_stop_session` | dispose the session (chain state is discarded) | +| `chasm_restart_session` | re-run from scratch: fresh chain, breakpoints kept, optional new script args | ### Navigation | Tool | Meaning | |---|---| -| `sevm_continue` | run to the next breakpoint/watchpoint/error, or program end | -| `sevm_step_opcodes` | `count` opcodes (≤1000) | -| `sevm_step_lines` / `sevm_next_lines` | Solidity lines, into / over internal calls | -| `sevm_finish_frame` | run to the end of the current frame | -| `sevm_run_to` | a pc, or a location like `Bank.sol:46` / `*0x108` | -| `sevm_get_status` | the stop report alone | -| `sevm_diff_since_last_stop` | what the last navigation changed | +| `chasm_continue` | run to the next breakpoint/watchpoint/error, or program end | +| `chasm_step_opcodes` | `count` opcodes (≤1000) | +| `chasm_step_lines` / `chasm_next_lines` | Solidity lines, into / over internal calls | +| `chasm_finish_frame` | run to the end of the current frame | +| `chasm_run_to` | a pc, or a location like `Bank.sol:46` / `*0x108` | +| `chasm_get_status` | the stop report alone | +| `chasm_diff_since_last_stop` | what the last navigation changed | ### Inspection (windowed, truncation-flagged) | Tool | Meaning | |---|---| -| `sevm_read_memory` | 32-byte words with region names. `beyond: true` marks unallocated reads as zero | -| `sevm_read_stack` | operand stack, index 0 = top | -| `sevm_read_storage` | decoded layout by default (names, types, packed slots), or raw slots | -| `sevm_read_calldata` | windowed. The first window includes selector and signature | -| `sevm_get_backtrace` | Solidity and EVM frames interleaved | -| `sevm_get_locals` / `sevm_get_arguments` | decoded locals / ABI-decoded call arguments | -| `sevm_disassemble` | rows `{pc, text, jumpdest, line}` around a pc | -| `sevm_read_source` | numbered source window, current line flagged | -| `sevm_get_gas_profile` | spend by opcode and by source line | -| `sevm_get_logs` | events so far, names decoded | -| `sevm_list_contracts` / `sevm_list_functions` | the compiled surface | +| `chasm_read_memory` | 32-byte words with region names. `beyond: true` marks unallocated reads as zero | +| `chasm_read_stack` | operand stack, index 0 = top | +| `chasm_read_storage` | decoded layout by default (names, types, packed slots), or raw slots | +| `chasm_read_calldata` | windowed. The first window includes selector and signature | +| `chasm_get_backtrace` | Solidity and EVM frames interleaved | +| `chasm_get_locals` / `chasm_get_arguments` | decoded locals / ABI-decoded call arguments | +| `chasm_disassemble` | rows `{pc, text, jumpdest, line}` around a pc | +| `chasm_read_source` | numbered source window, current line flagged | +| `chasm_get_gas_profile` | spend by opcode and by source line | +| `chasm_get_logs` | events so far, names decoded | +| `chasm_list_contracts` / `chasm_list_functions` | the compiled surface | ### Search, provenance, and bytecode parity | Tool | Meaning | |---|---| -| `sevm_find_bytes` | hex pattern in `code` (gadget hunting: pc, instruction alignment, nearest preceding JUMPDEST), `memory`, or `calldata` | -| `sevm_set_provenance` / `sevm_why_stack(index)` | record opcode steps within a frame and trace stack values to constants, calldata, memory, storage, and frame entry | -| `sevm_export_trace` | the recording as anvil/geth structLog JSON (to a file, or a windowed inline slice) | -| `sevm_check_parity` | compare a compiled runtime against deployed bytecode. `sevm_start_session(reference_runtime_hex=...)` stops on divergence | +| `chasm_find_bytes` | hex pattern in `code` (gadget hunting: pc, instruction alignment, nearest preceding JUMPDEST), `memory`, or `calldata` | +| `chasm_set_provenance` / `chasm_why_stack(index)` | record opcode steps within a frame and trace stack values to constants, calldata, memory, storage, and frame entry | +| `chasm_export_trace` | the recording as anvil/geth structLog JSON (to a file, or a windowed inline slice) | +| `chasm_check_parity` | compare a compiled runtime against deployed bytecode. `chasm_start_session(reference_runtime_hex=...)` stops on divergence | **Provenance semantics**: REVM pauses at each opcode while recording is enabled. The slice follows recorded values and positions within a frame. It skips the @@ -95,31 +95,31 @@ Hand mutations (`set $stack[i]`) are not recorded, so ask `why` before mutating. | Tool | Meaning | |---|---| -| `sevm_set_breakpoint` | `File.sol:LINE`, `*0xPC`, or a function name, with an optional Solidity `condition` | -| `sevm_set_opcode_breakpoint` | every occurrence of an opcode (`SSTORE`, `DELEGATECALL`, ...) | -| `sevm_set_watchpoint` | storage value written/read/accessed, or `*0x40` memory | -| `sevm_list_breakpoints` / `sevm_delete_breakpoint` | management | +| `chasm_set_breakpoint` | `File.sol:LINE`, `*0xPC`, or a function name, with an optional Solidity `condition` | +| `chasm_set_opcode_breakpoint` | every occurrence of an opcode (`SSTORE`, `DELEGATECALL`, ...) | +| `chasm_set_watchpoint` | storage value written/read/accessed, or `*0x40` memory | +| `chasm_list_breakpoints` / `chasm_delete_breakpoint` | management | ### Mutation and evaluation | Tool | Meaning | |---|---| -| `sevm_evaluate` | real Solidity against the paused frame (state, mappings, locals, `keccak256`, `abi.encode`, units). `keep: true` keeps side effects like gdb's `call` | -| `sevm_set_gas` | overwrite the gas meter. At an out-of-gas stop, continuing retries the failed instruction | -| `sevm_set_stack_slot` | rewrite an operand before its opcode consumes it | -| `sevm_write_memory` / `sevm_write_storage` | raw writes | -| `sevm_set_pc` | move the pc (JUMPDESTs only) | -| `sevm_command` | raw gdb-verb passthrough for everything else: `vm.*` cheatcodes, Yul, `jump`, `reseat`, `x/32xb 0x40` | +| `chasm_evaluate` | real Solidity against the paused frame (state, mappings, locals, `keccak256`, `abi.encode`, units). `keep: true` keeps side effects like gdb's `call` | +| `chasm_set_gas` | overwrite the gas meter. At an out-of-gas stop, continuing retries the failed instruction | +| `chasm_set_stack_slot` | rewrite an operand before its opcode consumes it | +| `chasm_write_memory` / `chasm_write_storage` | raw writes | +| `chasm_set_pc` | move the pc (JUMPDESTs only) | +| `chasm_command` | raw gdb-verb passthrough for everything else: `vm.*` cheatcodes, Yul, `jump`, `reseat`, `x/32xb 0x40` | ## A first session ``` -sevm_start_session(target="exploit.py", contracts="contracts/", optimize=true) -sevm_find_bytes(pattern="5b50505050", scope="code") # gadget hunt -sevm_set_breakpoint(location="*0x1a2") # or "deposit", "SSTORE" -sevm_continue() -sevm_read_memory(offset=0x40, words=4) -sevm_evaluate(expression="balances[msg.sender]") -sevm_diff_since_last_stop() -sevm_restart_session(args=["0x"]) +chasm_start_session(target="exploit.py", contracts="contracts/", optimize=true) +chasm_find_bytes(pattern="5b50505050", scope="code") # gadget hunt +chasm_set_breakpoint(location="*0x1a2") # or "deposit", "SSTORE" +chasm_continue() +chasm_read_memory(offset=0x40, words=4) +chasm_evaluate(expression="balances[msg.sender]") +chasm_diff_since_last_stop() +chasm_restart_session(args=["0x"]) ``` diff --git a/examples/README.md b/examples/README.md index c5d2121..ac5e327 100644 --- a/examples/README.md +++ b/examples/README.md @@ -10,9 +10,9 @@ examples/ ``` ```bash -sevm run examples/bank/test/Bank.t.sol # a Foundry test -sevm run -y examples/standalone/Vault.t.sol # clones forge-std first -sevm run --contracts examples/bank/src examples/debug_bank.py # a web3.py script +chasm run examples/bank/test/Bank.t.sol # a Foundry test +chasm run -y examples/standalone/Vault.t.sol # clones forge-std first +chasm run --contracts examples/bank/src examples/debug_bank.py # a web3.py script ``` Running the first two writes `lib/`, `out/`, `cache/` and `remappings.txt` next to the diff --git a/examples/bank/foundry.toml b/examples/bank/foundry.toml index 774639a..7ea83ab 100644 --- a/examples/bank/foundry.toml +++ b/examples/bank/foundry.toml @@ -2,5 +2,5 @@ src = "src" test = "test" libs = ["lib"] -# sevm forces the optimizer OFF for debuggable builds regardless of this file. +# chasm forces the optimizer OFF for debuggable builds regardless of this file. optimizer = false diff --git a/examples/debug_bank.py b/examples/debug_bank.py index 0188e53..b402153 100644 --- a/examples/debug_bank.py +++ b/examples/debug_bank.py @@ -5,7 +5,7 @@ from eth_account import Account from web3 import EthereumTesterProvider, Web3 -from sevm.compile import compile_project +from chasm_debugger.compile import compile_project CONTRACTS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "bank", "src") @@ -17,7 +17,7 @@ def main(): art = project.artifact("Bank") factory = w3.eth.contract(abi=art.abi, bytecode=art.bytecode.hex()) - tx = factory.constructor("sevm-bank").transact( + tx = factory.constructor("chasm-bank").transact( {"value": w3.to_wei(1, "ether"), "gas": 3_000_000} ) bank_address = w3.eth.wait_for_transaction_receipt(tx)["contractAddress"] diff --git a/pyproject.toml b/pyproject.toml index 7c61ac6..43530db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,9 +3,9 @@ requires = ["maturin>=1.10,<2.0"] build-backend = "maturin" [project] -name = "sevm" +name = "chasm-debugger" version = "0.1.0" -description = "sevm is an interactive, gdb-style Solidity/EVM debugger on REVM, built for red-team dynamic analysis." +description = "Chasm is an interactive, gdb-style Solidity/EVM debugger on REVM, built for red-team dynamic analysis." readme = "README.md" requires-python = ">=3.10" license = "MIT" @@ -21,13 +21,13 @@ classifiers = [ ] # Runtime dependencies. Every third-party package imported by the sources under -# src/sevm is declared here (not left to transitive resolution). `eth`, `eth_abi`, -# `eth_utils` and `eth_account` all arrive with web3, but sevm imports them directly, +# src/chasm_debugger is declared here (not left to transitive resolution). `eth`, `eth_abi`, +# `eth_utils` and `eth_account` all arrive with web3, but chasm imports them directly, # so they are named explicitly. # -# Do not use `web3[tester]`: sevm supplies its own REVM provider, and the tester extra +# Do not use `web3[tester]`: chasm supplies its own REVM provider, and the tester extra # would restore the archived Py-EVM stack. The pycryptodome backend is named directly so -# the keccak implementation sevm relies on is not left to a transitive dependency. +# the keccak implementation chasm relies on is not left to a transitive dependency. dependencies = [ "web3>=7.0", "eth-hash[pycryptodome]>=0.5.1", @@ -47,16 +47,16 @@ dependencies = [ ] [project.scripts] -sevm = "sevm.cli:main" -sevm-engine = "sevm.revm_server:main" +chasm = "chasm_debugger.cli:main" +chasm-engine = "chasm_debugger.revm_server:main" [project.urls] -Homepage = "https://github.com/your-org/sevm" -Documentation = "https://github.com/your-org/sevm/blob/main/README.md" +Homepage = "https://github.com/ChasmHQ/debugger" +Documentation = "https://github.com/ChasmHQ/debugger/blob/main/README.md" [tool.maturin] manifest-path = "rust/python/Cargo.toml" -module-name = "sevm._revm" +module-name = "chasm_debugger._revm" python-source = "src" [dependency-groups] @@ -70,10 +70,10 @@ dev = [ ] [tool.pytest.ini_options] -markers = ["network: hits the network; run with SEVM_NETWORK_TESTS=1"] +markers = ["network: hits the network; run with CHASM_NETWORK_TESTS=1"] testpaths = ["tests"] # Put tests/ on sys.path so the shared `harness` module imports cleanly, and rely -# on the installed (editable) `sevm` package for `import sevm.*` - no sys.path hacks. +# on the installed (editable) `chasm_debugger` package - no sys.path hacks. pythonpath = ["tests"] addopts = "-ra" diff --git a/rust/core/Cargo.toml b/rust/core/Cargo.toml index b9c1fbc..b40e9ed 100644 --- a/rust/core/Cargo.toml +++ b/rust/core/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "sevm-revm-core" -description = "REVM debugger core for sevm" +name = "chasm-revm-core" +description = "REVM debugger core for chasm" version.workspace = true edition.workspace = true rust-version.workspace = true diff --git a/rust/core/src/engine.rs b/rust/core/src/engine.rs index 9636939..2b07ebf 100644 --- a/rust/core/src/engine.rs +++ b/rust/core/src/engine.rs @@ -42,7 +42,7 @@ const STATE_RUNNING: u8 = 1; const STATE_PAUSED: u8 = 2; const STATE_STOPPED: u8 = 3; -type SevmContext = Context; +type ChasmContext = Context; enum EngineCommand { Transact(TransactionRequest), @@ -64,7 +64,7 @@ struct StepBackup { gas: Gas, } -struct SevmInspector { +struct ChasmInspector { commands: Receiver, events: Sender, breakpoints: HashSet<(Address, usize)>, @@ -81,7 +81,7 @@ struct SevmInspector { steps_until_pause: Option, } -impl SevmInspector { +impl ChasmInspector { fn snapshot(&self, interpreter: &Interpreter, reason: PauseReason) -> Snapshot { let address = interpreter.input.target_address(); let code_address = interpreter @@ -145,7 +145,7 @@ impl SevmInspector { fn pause( &mut self, interpreter: &mut Interpreter, - context: &mut SevmContext, + context: &mut ChasmContext, reason: PauseReason, ) -> bool { self.state.store(STATE_PAUSED, Ordering::Release); @@ -319,7 +319,7 @@ impl SevmInspector { ) } - fn host_call(&mut self, context: &mut SevmContext, inputs: &CallInputs) -> CallOutcome { + fn host_call(&mut self, context: &mut ChasmContext, inputs: &CallInputs) -> CallOutcome { self.state.store(STATE_PAUSED, Ordering::Release); let event = HostCall { address: inputs.bytecode_address, @@ -374,7 +374,7 @@ impl SevmInspector { call_result(inputs, Bytes::new(), true) } - fn apply_prank(&mut self, context: &mut SevmContext, inputs: &mut CallInputs) { + fn apply_prank(&mut self, context: &mut ChasmContext, inputs: &mut CallInputs) { let Some(prank) = self.prank.clone() else { return; }; @@ -425,7 +425,7 @@ fn call_result(inputs: &CallInputs, output: Bytes, revert: bool) -> CallOutcome } fn execute_state_command( - context: &mut SevmContext, + context: &mut ChasmContext, command: StateCommand, ) -> Result { let invalid = |error| SessionError::InvalidCommand(format!("{error:?}")); @@ -582,7 +582,7 @@ fn execute_state_command( #[allow(clippy::too_many_arguments)] fn execute_evaluation( - context: &mut SevmContext, + context: &mut ChasmContext, interpreter: &Interpreter, code: Bytes, data: Bytes, @@ -608,7 +608,7 @@ fn execute_evaluation( gas_limit, ); let instructions = - EthInstructions::::new_mainnet_with_spec(SpecId::CANCUN); + EthInstructions::::new_mainnet_with_spec(SpecId::CANCUN); let action = nested.run_plain( instructions.instruction_table(), instructions.gas_table(), @@ -631,7 +631,7 @@ fn execute_evaluation( } fn execute_opcode( - context: &mut SevmContext, + context: &mut ChasmContext, interpreter: &mut Interpreter, opcode: u8, arguments: Vec, @@ -669,7 +669,7 @@ fn execute_opcode( let checkpoint = context.journal_mut().checkpoint(); let instructions = - EthInstructions::::new_mainnet_with_spec(SpecId::CANCUN); + EthInstructions::::new_mainnet_with_spec(SpecId::CANCUN); let result = (|| { let static_gas = instructions.gas_table()[opcode as usize] as u64; if !nested.gas.record_regular_cost(static_gas) { @@ -713,10 +713,10 @@ fn execute_opcode( result } -impl Inspector for SevmInspector { +impl Inspector for ChasmInspector { fn frame_start( &mut self, - context: &mut SevmContext, + context: &mut ChasmContext, frame_input: &mut FrameInput, ) -> Option { let depth = self.frames.len(); @@ -769,7 +769,7 @@ impl Inspector for SevmInspector { fn initialize_interp( &mut self, interpreter: &mut Interpreter, - _context: &mut SevmContext, + _context: &mut ChasmContext, ) { if let Some(frame) = self.frames.last_mut() { Self::update_frame(frame, interpreter); @@ -778,7 +778,7 @@ impl Inspector for SevmInspector { fn frame_end( &mut self, - context: &mut SevmContext, + context: &mut ChasmContext, _frame_input: &FrameInput, _frame_result: &mut revm::handler::FrameResult, ) { @@ -789,7 +789,7 @@ impl Inspector for SevmInspector { } } - fn step(&mut self, interpreter: &mut Interpreter, context: &mut SevmContext) { + fn step(&mut self, interpreter: &mut Interpreter, context: &mut ChasmContext) { self.step_index = self.step_index.saturating_add(1); if let Some(frame) = self.frames.last_mut() { Self::update_frame(frame, interpreter); @@ -830,7 +830,7 @@ impl Inspector for SevmInspector { fn step_end( &mut self, interpreter: &mut Interpreter, - context: &mut SevmContext, + context: &mut ChasmContext, ) { let result = interpreter .bytecode @@ -873,7 +873,7 @@ impl Inspector for SevmInspector { self.backup = None; } - fn call(&mut self, context: &mut SevmContext, inputs: &mut CallInputs) -> Option { + fn call(&mut self, context: &mut ChasmContext, inputs: &mut CallInputs) -> Option { if matches!(inputs.bytecode_address, CHEATCODE_ADDRESS | CONSOLE_ADDRESS) { return Some(self.host_call(context, inputs)); } @@ -1158,7 +1158,7 @@ fn run_worker( .map_err(|error| format!("{error:?}"))?; } } - let inspector = SevmInspector { + let inspector = ChasmInspector { commands: commands.clone(), events: events.clone(), breakpoints: config diff --git a/rust/core/tests/prototype.rs b/rust/core/tests/prototype.rs index 376b65e..47fd25b 100644 --- a/rust/core/tests/prototype.rs +++ b/rust/core/tests/prototype.rs @@ -2,7 +2,7 @@ use revm::{ bytecode::opcode, primitives::{Address, B256, Bytes, U256, address, keccak256}, }; -use sevm_revm_core::{ +use chasm_revm_core::{ AccountSpec, Breakpoint, CHEATCODE_ADDRESS, ChainConfig, CommandValue, DEFAULT_CALLER, DEFAULT_TARGET, DebugEngine, DebugEvent, FrameKind, PauseReason, PrankConfig, PrototypeSession, SessionConfig, StateCommand, TransactionRequest, @@ -11,14 +11,14 @@ use std::time::Duration; const TIMEOUT: Duration = Duration::from_secs(5); -fn paused(session: &PrototypeSession) -> sevm_revm_core::Snapshot { +fn paused(session: &PrototypeSession) -> chasm_revm_core::Snapshot { match session.wait(TIMEOUT).unwrap() { DebugEvent::Paused(snapshot) => *snapshot, event => panic!("expected pause, got {event:?}"), } } -fn finished(session: &PrototypeSession) -> sevm_revm_core::Finished { +fn finished(session: &PrototypeSession) -> chasm_revm_core::Finished { match session.wait(TIMEOUT).unwrap() { DebugEvent::Finished(result) => result, event => panic!("expected finish, got {event:?}"), diff --git a/rust/headless/Cargo.toml b/rust/headless/Cargo.toml index 8ee5baa..3915dff 100644 --- a/rust/headless/Cargo.toml +++ b/rust/headless/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "sevm-revm-headless" -description = "Transport-neutral JSON-RPC server for the sevm REVM engine" +name = "chasm-revm-headless" +description = "Transport-neutral JSON-RPC server for the chasm REVM engine" version.workspace = true edition.workspace = true rust-version.workspace = true @@ -10,4 +10,4 @@ license.workspace = true revm.workspace = true serde = { version = "1.0.229", features = ["derive"] } serde_json = "1.0.151" -sevm-revm-core.workspace = true +chasm-revm-core.workspace = true diff --git a/rust/headless/src/lib.rs b/rust/headless/src/lib.rs index 597dbcd..56d1102 100644 --- a/rust/headless/src/lib.rs +++ b/rust/headless/src/lib.rs @@ -1,7 +1,7 @@ use revm::primitives::{Address, B256, Bytes, U256}; use serde::{Deserialize, Serialize, de::DeserializeOwned}; use serde_json::{Value, json}; -use sevm_revm_core::{ +use chasm_revm_core::{ AccountSpec, Breakpoint, ChainConfig, CommandValue, DEFAULT_CALLER, DebugCommand, DebugEngine, DebugEvent, FrameContext, FrameKind, PauseReason, PrankConfig, SessionConfig, SessionError, Snapshot, StateCommand, TransactionKind, TransactionRequest, @@ -13,7 +13,7 @@ use std::{ time::Duration, }; -pub const PROTOCOL_VERSION: &str = "sevm-debugger/1"; +pub const PROTOCOL_VERSION: &str = "chasm-debugger/1"; const PARSE_ERROR: i32 = -32700; const INVALID_REQUEST: i32 = -32600; diff --git a/rust/headless/src/main.rs b/rust/headless/src/main.rs index 325869a..e8d77d0 100644 --- a/rust/headless/src/main.rs +++ b/rust/headless/src/main.rs @@ -3,8 +3,8 @@ use std::io; fn main() { let stdin = io::stdin(); let stdout = io::stdout(); - if let Err(error) = sevm_revm_headless::serve(stdin.lock(), stdout.lock()) { - eprintln!("sevm REVM headless server failed: {error}"); + if let Err(error) = chasm_revm_headless::serve(stdin.lock(), stdout.lock()) { + eprintln!("chasm REVM headless server failed: {error}"); std::process::exit(1); } } diff --git a/rust/headless/tests/protocol.rs b/rust/headless/tests/protocol.rs index 0ee6ac6..c6e85dc 100644 --- a/rust/headless/tests/protocol.rs +++ b/rust/headless/tests/protocol.rs @@ -1,6 +1,6 @@ use revm::primitives::keccak256; use serde_json::{Value, json}; -use sevm_revm_headless::{PROTOCOL_VERSION, serve}; +use chasm_revm_headless::{PROTOCOL_VERSION, serve}; use std::{ io::{Cursor, Read, Write}, process::{Command, Stdio}, @@ -102,7 +102,7 @@ fn reports_protocol_and_recovers_after_bad_input() { #[test] fn binary_serves_the_protocol_on_stdio() { - let mut child = Command::new(env!("CARGO_BIN_EXE_sevm-revm-headless")) + let mut child = Command::new(env!("CARGO_BIN_EXE_chasm-revm-headless")) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn() diff --git a/rust/python/Cargo.toml b/rust/python/Cargo.toml index da6f688..8e99422 100644 --- a/rust/python/Cargo.toml +++ b/rust/python/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "sevm-revm-python" -description = "Python bridge for the sevm REVM debugger core" +name = "chasm-revm-python" +description = "Python bridge for the chasm REVM debugger core" version.workspace = true edition.workspace = true rust-version.workspace = true @@ -13,5 +13,5 @@ crate-type = ["cdylib"] [dependencies] pyo3 = { version = "0.29.2", features = ["abi3-py310", "extension-module"] } revm.workspace = true -sevm-revm-core.workspace = true -sevm-revm-headless.workspace = true +chasm-revm-core.workspace = true +chasm-revm-headless.workspace = true diff --git a/rust/python/src/lib.rs b/rust/python/src/lib.rs index 5132c13..bd9dbd6 100644 --- a/rust/python/src/lib.rs +++ b/rust/python/src/lib.rs @@ -7,7 +7,7 @@ use revm::{ bytecode::OpCode, primitives::{Address, B256, Bytes, U256}, }; -use sevm_revm_core::{ +use chasm_revm_core::{ AccountSpec, Breakpoint, CHEATCODE_ADDRESS, CONSOLE_ADDRESS, ChainConfig, CommandValue, DEFAULT_CALLER, DEFAULT_TARGET, DebugEngine, DebugEvent, FrameContext, FrameKind, PauseReason, PrankConfig, SessionConfig, SessionError, Snapshot, StateCommand, TransactionRequest, @@ -212,7 +212,7 @@ fn event_dict<'py>(py: Python<'py>, event: DebugEvent) -> PyResult) -> PyResult<()> { py.detach(|| { let stdin = io::stdin(); let stdout = io::stdout(); - sevm_revm_headless::serve(stdin.lock(), stdout.lock()) + chasm_revm_headless::serve(stdin.lock(), stdout.lock()) }) .map_err(|error| PyRuntimeError::new_err(error.to_string())) } diff --git a/src/sevm/__init__.py b/src/chasm_debugger/__init__.py similarity index 81% rename from src/sevm/__init__.py rename to src/chasm_debugger/__init__.py index 1e3660f..1662c07 100644 --- a/src/sevm/__init__.py +++ b/src/chasm_debugger/__init__.py @@ -1,4 +1,4 @@ -"""sevm: an interactive EVM playground on REVM, built for red-team dynamic analysis. +"""Chasm: an interactive EVM playground on REVM, built for red-team dynamic analysis. Where things live: @@ -13,19 +13,19 @@ tui/ the Textual fullscreen frontend console.py the plain-text frontend (`--console`) - artifacts.py forge-shaped `out/sevm//.json` + artifacts.py forge-shaped `out/chasm//.json` breakpoints.py the breakpoint and watchpoint set cache.py the on-disk build cache clipboard.py copying through the platform's own tool decode.py storage layout, calldata and revert decoding disasm.py the opcode table and disassembler - doctor.py what `sevm doctor` reports: compiler, runtimes, caches + doctor.py what `chasm doctor` reports: compiler, runtimes, caches dispatch.py the external dispatcher read back out of runtime bytecode foundry.py the Foundry test runner frames.py EVM and Solidity frames, and the snapshot handed to the UI libs.py dependency resolution: imports -> repo -> clone -> remapping provider.py the REVM-backed Web3 provider for Python driver scripts - revm_server.py the headless REVM JSON-RPC process (`sevm-engine`) + revm_server.py the headless REVM JSON-RPC process (`chasm-engine`) srcmap.py solc source maps: pc <-> source location """ diff --git a/src/sevm/__main__.py b/src/chasm_debugger/__main__.py similarity index 100% rename from src/sevm/__main__.py rename to src/chasm_debugger/__main__.py diff --git a/src/sevm/artifacts.py b/src/chasm_debugger/artifacts.py similarity index 92% rename from src/sevm/artifacts.py rename to src/chasm_debugger/artifacts.py index 0167e04..acfd11e 100644 --- a/src/sevm/artifacts.py +++ b/src/chasm_debugger/artifacts.py @@ -1,10 +1,10 @@ """Forge-shaped artifacts on disk, so a build leaves something readable behind. The layout and field names are forge's own (`/.json`), nested one level -under the project's `out` directory. That nesting is deliberate: sevm compiles with the +under the project's `out` directory. That nesting is deliberate: chasm compiles with the optimizer off, forge's cache would still call an overwritten artifact fresh, and the next -`forge test` would silently run sevm's build. `metadata`/`rawMetadata` are the one thing -forge writes and this does not; sevm never asks solc for them. +`forge test` would silently run chasm's build. `metadata`/`rawMetadata` are the one thing +forge writes and this does not; chasm never asks solc for them. """ from __future__ import annotations @@ -16,7 +16,7 @@ if TYPE_CHECKING: from .compile import Artifact, Project -SUBDIR = "sevm" +SUBDIR = "chasm" def out_dir(root: str, out: str = "out") -> str: diff --git a/src/sevm/assembly/__init__.py b/src/chasm_debugger/assembly/__init__.py similarity index 100% rename from src/sevm/assembly/__init__.py rename to src/chasm_debugger/assembly/__init__.py diff --git a/src/sevm/assembly/builtins.py b/src/chasm_debugger/assembly/builtins.py similarity index 100% rename from src/sevm/assembly/builtins.py rename to src/chasm_debugger/assembly/builtins.py diff --git a/src/sevm/assembly/execute.py b/src/chasm_debugger/assembly/execute.py similarity index 100% rename from src/sevm/assembly/execute.py rename to src/chasm_debugger/assembly/execute.py diff --git a/src/sevm/assembly/parser.py b/src/chasm_debugger/assembly/parser.py similarity index 100% rename from src/sevm/assembly/parser.py rename to src/chasm_debugger/assembly/parser.py diff --git a/src/sevm/breakpoints.py b/src/chasm_debugger/breakpoints.py similarity index 100% rename from src/sevm/breakpoints.py rename to src/chasm_debugger/breakpoints.py diff --git a/src/sevm/cache.py b/src/chasm_debugger/cache.py similarity index 96% rename from src/sevm/cache.py rename to src/chasm_debugger/cache.py index 4acb056..e2aaa0e 100644 --- a/src/sevm/cache.py +++ b/src/chasm_debugger/cache.py @@ -41,17 +41,17 @@ def user_cache_dir() -> str: base = os.environ.get("XDG_CACHE_HOME") or os.path.expanduser("~/.cache") - return os.path.join(base, "sevm") + return os.path.join(base, "chasm") def cache_dir(root: str) -> str: """Where this root's units live. - A Foundry project gets `cache/sevm/`, which its .gitignore already covers and - `forge clean` already removes. Anywhere else sevm keeps out of the directory. + A Foundry project gets `cache/chasm/`, which its .gitignore already covers and + `forge clean` already removes. Anywhere else chasm keeps out of the directory. """ if os.path.isfile(os.path.join(root, "foundry.toml")): - return os.path.join(root, "cache", "sevm") + return os.path.join(root, "cache", "chasm") digest = hashlib.sha256(os.path.abspath(root).encode()).hexdigest()[:16] return os.path.join(user_cache_dir(), "projects", digest) @@ -232,7 +232,7 @@ def _prune(self, units: dict) -> None: def open_cache(root: str, enabled: bool = True) -> BuildCache | None: - if not enabled or os.environ.get("SEVM_NO_CACHE"): + if not enabled or os.environ.get("CHASM_NO_CACHE"): return None return BuildCache(cache_dir(root)) diff --git a/src/sevm/cheatcodes/__init__.py b/src/chasm_debugger/cheatcodes/__init__.py similarity index 100% rename from src/sevm/cheatcodes/__init__.py rename to src/chasm_debugger/cheatcodes/__init__.py diff --git a/src/sevm/cheatcodes/args.py b/src/chasm_debugger/cheatcodes/args.py similarity index 100% rename from src/sevm/cheatcodes/args.py rename to src/chasm_debugger/cheatcodes/args.py diff --git a/src/sevm/cheatcodes/assertions.py b/src/chasm_debugger/cheatcodes/assertions.py similarity index 100% rename from src/sevm/cheatcodes/assertions.py rename to src/chasm_debugger/cheatcodes/assertions.py diff --git a/src/sevm/cheatcodes/cheats.py b/src/chasm_debugger/cheatcodes/cheats.py similarity index 96% rename from src/sevm/cheatcodes/cheats.py rename to src/chasm_debugger/cheatcodes/cheats.py index 9e9c0f7..3149201 100644 --- a/src/sevm/cheatcodes/cheats.py +++ b/src/chasm_debugger/cheatcodes/cheats.py @@ -236,21 +236,21 @@ def _warm_slot(ctx: CheatContext) -> None: # ---- gas metering / control: no-ops that keep a test running ---------------------------- -# sevm meters and reports gas itself; these forge knobs only affect gas *snapshots*, which -# sevm does not produce, so accepting them as no-ops lets a test that toggles them run. +# chasm meters and reports gas itself; these forge knobs only affect gas *snapshots*, which +# chasm does not produce, so accepting them as no-ops lets a test that toggles them run. -@_cheat("pauseGasMetering()", doc="no-op (sevm does not take gas snapshots)") +@_cheat("pauseGasMetering()", doc="no-op (chasm does not take gas snapshots)") def _pause_gas(ctx: CheatContext) -> None: return None -@_cheat("resumeGasMetering()", doc="no-op (sevm does not take gas snapshots)") +@_cheat("resumeGasMetering()", doc="no-op (chasm does not take gas snapshots)") def _resume_gas(ctx: CheatContext) -> None: return None -@_cheat("resetGasMetering()", doc="no-op (sevm does not take gas snapshots)") +@_cheat("resetGasMetering()", doc="no-op (chasm does not take gas snapshots)") def _reset_gas(ctx: CheatContext) -> None: return None diff --git a/src/sevm/cheatcodes/console.py b/src/chasm_debugger/cheatcodes/console.py similarity index 100% rename from src/sevm/cheatcodes/console.py rename to src/chasm_debugger/cheatcodes/console.py diff --git a/src/sevm/cheatcodes/convert.py b/src/chasm_debugger/cheatcodes/convert.py similarity index 100% rename from src/sevm/cheatcodes/convert.py rename to src/chasm_debugger/cheatcodes/convert.py diff --git a/src/sevm/cheatcodes/env.py b/src/chasm_debugger/cheatcodes/env.py similarity index 98% rename from src/sevm/cheatcodes/env.py rename to src/chasm_debugger/cheatcodes/env.py index c62996a..706f618 100644 --- a/src/sevm/cheatcodes/env.py +++ b/src/chasm_debugger/cheatcodes/env.py @@ -1,7 +1,7 @@ """The `vm.env*` family: read process environment variables into Solidity values. These read `os.environ`, exactly as forge reads the process environment, so a value is set -by exporting it before launching sevm (`payload=0x... uv run sevm run ...`) or with +by exporting it before launching chasm (`payload=0x... uv run chasm run ...`) or with `vm.setEnv` at the prompt. Each type has a scalar form and a `(name, delimiter)` array form; `envOr` returns a caller-supplied default instead of reverting when the key is absent. """ diff --git a/src/sevm/cheatcodes/registry.py b/src/chasm_debugger/cheatcodes/registry.py similarity index 99% rename from src/sevm/cheatcodes/registry.py rename to src/chasm_debugger/cheatcodes/registry.py index aa15740..a0aa7bc 100644 --- a/src/sevm/cheatcodes/registry.py +++ b/src/chasm_debugger/cheatcodes/registry.py @@ -1,7 +1,7 @@ """The cheat registry, and the dispatch that runs one. Foundry cheatcodes are calls to the magic address `0x7109709E...` (the last 20 bytes of -keccak256("hevm cheat code")). A real EVM does not know it; Foundry and sevm intercept the +keccak256("hevm cheat code")). A real EVM does not know it; Foundry and chasm intercept the call and interpret it. `apply_cheat` decodes the selector and arguments, mutates live REVM state or session bookkeeping, and returns ABI-encoded data to the caller. diff --git a/src/sevm/cheatcodes/wallet.py b/src/chasm_debugger/cheatcodes/wallet.py similarity index 100% rename from src/sevm/cheatcodes/wallet.py rename to src/chasm_debugger/cheatcodes/wallet.py diff --git a/src/sevm/cli.py b/src/chasm_debugger/cli.py similarity index 96% rename from src/sevm/cli.py rename to src/chasm_debugger/cli.py index f66758a..86d9254 100644 --- a/src/sevm/cli.py +++ b/src/chasm_debugger/cli.py @@ -1,7 +1,7 @@ -"""`sevm run script.py` - the entry point. +"""`chasm run script.py` - the entry point. By decision, the debugger attaches to the user's own script rather than owning the -setup. The script drives web3 exactly as it already does; sevm compiles the contracts, +setup. The script drives web3 exactly as it already does; chasm compiles the contracts, temporarily supplies its REVM-backed Web3 provider, runs the script on the VM thread, and stops the first time execution enters code it recognises. @@ -55,7 +55,7 @@ def target() -> None: def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( - prog="sevm", + prog="chasm", description="A gdb-style interactive Solidity/EVM debugger.", ) sub = parser.add_subparsers(dest="command", required=True) @@ -63,7 +63,7 @@ def build_parser() -> argparse.ArgumentParser: run = sub.add_parser( "run", help="debug a Python script that drives web3", - usage="sevm run [options] SCRIPT [script args...]", + usage="chasm run [options] SCRIPT [script args...]", epilog="Options must come BEFORE the script; everything after it is passed to the script.", ) run.add_argument( @@ -81,7 +81,7 @@ def build_parser() -> argparse.ArgumentParser: "--solc-binary", default=None, metavar="PATH", - help="use this solc executable instead of downloading one (env: SEVM_SOLC)", + help="use this solc executable instead of downloading one (env: CHASM_SOLC)", ) run.add_argument( "--console", action="store_true", help="plain text frontend instead of the TUI" @@ -138,7 +138,7 @@ def build_parser() -> argparse.ArgumentParser: "--force", action="store_true", help="recompile even if the cache has this build" ) compile_cmd = sub.add_parser( - "compile", help="compile contracts and report what sevm sees" + "compile", help="compile contracts and report what chasm sees" ) compile_cmd.add_argument("contracts", help="directory or file of .sol sources") compile_cmd.add_argument("--solc", default=None) @@ -164,14 +164,14 @@ def build_parser() -> argparse.ArgumentParser: ) sub.add_parser( - "doctor", help="report the compiler, runtimes and caches sevm found here" + "doctor", help="report the compiler, runtimes and caches chasm found here" ) mcp_cmd = sub.add_parser( "mcp", help="run the MCP server for AI clients (stdio transport)", description=( - "Run sevm as a Model Context Protocol server over stdio. An MCP client " + "Run chasm as a Model Context Protocol server over stdio. An MCP client " "starts/steps debug sessions and reads structured, windowed state — " "see docs/mcp.md for the tool list and client configuration." ), @@ -187,7 +187,7 @@ def build_parser() -> argparse.ArgumentParser: def cmd_doctor(_args: argparse.Namespace) -> int: - """Print what sevm found, and fail if none of it can compile.""" + """Print what chasm found, and fail if none of it can compile.""" from . import doctor lines = doctor.report() @@ -253,7 +253,7 @@ def cmd_run(args: argparse.Namespace) -> int: console.print(f"[bold red]no such script:[/bold red] {script}") return 1 - # Everything after the script is forwarded to it verbatim, so a sevm option placed + # Everything after the script is forwarded to it verbatim, so a chasm option placed # there silently does nothing; name that failure instead of leaving it confusing. stray = [ a @@ -278,8 +278,8 @@ def cmd_run(args: argparse.Namespace) -> int: if stray: console.print( f"[yellow]note:[/yellow] {' '.join(stray)} came after the script, so " - "it was passed to the script rather than to sevm. Options go before it:\n" - f" sevm run {' '.join(stray)} {script}" + "it was passed to the script rather than to chasm. Options go before it:\n" + f" chasm run {' '.join(stray)} {script}" ) # Dispatch by extension: a Foundry Solidity test, or the classic web3 Python driver. @@ -509,7 +509,7 @@ def _debug( return 0 if session.exit_error is None else 1 try: - from .tui.app import SevmApp + from .tui.app import ChasmApp except ImportError as exc: console.print( f"[yellow]the TUI needs Textual ({exc}). Install it with " @@ -520,7 +520,7 @@ def _debug( ConsoleFrontend(session, evaluator).run(first_event=first) return 0 if session.exit_error is None else 1 - app = SevmApp( + app = ChasmApp( session, evaluator, first_event=first, startup_commands=startup_commands ) # Mouse on: every pane renders Textual `Content`, which the framework can diff --git a/src/sevm/clipboard.py b/src/chasm_debugger/clipboard.py similarity index 100% rename from src/sevm/clipboard.py rename to src/chasm_debugger/clipboard.py diff --git a/src/sevm/commands/__init__.py b/src/chasm_debugger/commands/__init__.py similarity index 100% rename from src/sevm/commands/__init__.py rename to src/chasm_debugger/commands/__init__.py diff --git a/src/sevm/commands/breaking.py b/src/chasm_debugger/commands/breaking.py similarity index 100% rename from src/sevm/commands/breaking.py rename to src/chasm_debugger/commands/breaking.py diff --git a/src/sevm/commands/execution.py b/src/chasm_debugger/commands/execution.py similarity index 100% rename from src/sevm/commands/execution.py rename to src/chasm_debugger/commands/execution.py diff --git a/src/sevm/commands/help.py b/src/chasm_debugger/commands/help.py similarity index 95% rename from src/sevm/commands/help.py rename to src/chasm_debugger/commands/help.py index d0dbb1d..8b32b5d 100644 --- a/src/sevm/commands/help.py +++ b/src/chasm_debugger/commands/help.py @@ -57,7 +57,7 @@ a bare local name writes its stack slot: [dim]set var fee = 1 ether[/dim] [cyan]set[/cyan] $pc = 0x108 jump; [cyan]set[/cyan] $gas = N; [cyan]set[/cyan] $stack[0] = V; [cyan]set[/cyan] $storage[1] = V [cyan]reseat[/cyan] name the frame at the current pc after a JOP/manual jump - [cyan]bind[/cyan] x = $stack[3] pin a local to a stack slot sevm could not observe + [cyan]bind[/cyan] x = $stack[3] pin a local to a stack slot chasm could not observe [bold]Assembly[/bold] [cyan]mstore[/cyan](0x80, 1) type a builtin call straight at the prompt @@ -129,7 +129,7 @@ implementation's JUMPDEST, which is the pc every caller converges on, so `b *0x544` catches a proxy, a raw-calldata call and an internal call alike. -Both work on code sevm never compiled: a target already qualified as a signature or a +Both work on code chasm never compiled: a target already qualified as a signature or a selector is read straight off the running bytecode, ABI or no ABI. Add `Contract.` to ask about a contract other than the one in view. """, @@ -199,7 +199,7 @@ reseat re-name the Solidity frame at the current pc, after a reseat 7 --push JOP gadget or manual `jump` landed outside solc's calling convention (optional frame base; --push nests) - bind arr = $stack[3] pin a local sevm could not observe to a stack slot + bind arr = $stack[3] pin a local chasm could not observe to a stack slot (n counts from the top, as the STACK pane shows) mstore(0x80, 1) raw assembly; see `help assembly` @@ -221,7 +221,7 @@ b LOC if amount > 1 conditions over them set var fee = 1 ether writes the local's stack slot -solc emits no location info for locals; sevm reconstructs it from the AST (name, type, +solc emits no location info for locals; chasm reconstructs it from the AST (name, type, scope) plus the stack slot at the current pc. A local shadows a state variable of the same name, as in the contract. @@ -312,16 +312,16 @@ def _cheatcode_help() -> str: HELP_TOPICS["yul"] = HELP_TOPICS["assembly"] HELP_TOPICS["foundry"] = """ [bold]Foundry projects and libraries[/bold] -sevm compiles a `.t.sol` the way forge does, and installs what is missing first. +chasm compiles a `.t.sol` the way forge does, and installs what is missing first. - sevm run test/Counter.t.sol inside a project: its foundry.toml, remappings.txt + chasm run test/Counter.t.sol inside a project: its foundry.toml, remappings.txt and lib/ are used as they are - sevm run /tmp/scratch/Demo.t.sol outside one: writes foundry.toml, clones forge-std + chasm run /tmp/scratch/Demo.t.sol outside one: writes foundry.toml, clones forge-std into lib/, appends remappings.txt - sevm run -y ... skip the confirmation prompt - sevm run --no-install ... resolve from disk only; refuse a missing import + chasm run -y ... skip the confirmation prompt + chasm run --no-install ... resolve from disk only; refuse a missing import -An unresolved import is looked up in sevm's table, then on npm, and cloned from its git +An unresolved import is looked up in chasm's table, then on npm, and cloned from its git repository at the newest release tag: import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; diff --git a/src/sevm/commands/info.py b/src/chasm_debugger/commands/info.py similarity index 100% rename from src/sevm/commands/info.py rename to src/chasm_debugger/commands/info.py diff --git a/src/sevm/commands/inspecting.py b/src/chasm_debugger/commands/inspecting.py similarity index 100% rename from src/sevm/commands/inspecting.py rename to src/chasm_debugger/commands/inspecting.py diff --git a/src/sevm/commands/misc.py b/src/chasm_debugger/commands/misc.py similarity index 100% rename from src/sevm/commands/misc.py rename to src/chasm_debugger/commands/misc.py diff --git a/src/sevm/commands/mutation.py b/src/chasm_debugger/commands/mutation.py similarity index 98% rename from src/sevm/commands/mutation.py rename to src/chasm_debugger/commands/mutation.py index c881706..5892274 100644 --- a/src/sevm/commands/mutation.py +++ b/src/chasm_debugger/commands/mutation.py @@ -133,7 +133,7 @@ def cmd_reseat(proc: CommandProcessor, args: list[str], rest: str) -> CommandRes def cmd_bind(proc: CommandProcessor, args: list[str], rest: str) -> CommandResult: """`bind = $stack[]`: pin a local to a stack slot by hand. - For a local sevm could not observe because the frame was entered outside the + For a local chasm could not observe because the frame was entered outside the prologue (a JOP landing). `n` counts from the top, as the STACK pane shows it. """ body = rest.strip() diff --git a/src/sevm/commands/parsing.py b/src/chasm_debugger/commands/parsing.py similarity index 100% rename from src/sevm/commands/parsing.py rename to src/chasm_debugger/commands/parsing.py diff --git a/src/sevm/commands/processor.py b/src/chasm_debugger/commands/processor.py similarity index 100% rename from src/sevm/commands/processor.py rename to src/chasm_debugger/commands/processor.py diff --git a/src/sevm/commands/render.py b/src/chasm_debugger/commands/render.py similarity index 100% rename from src/sevm/commands/render.py rename to src/chasm_debugger/commands/render.py diff --git a/src/sevm/commands/result.py b/src/chasm_debugger/commands/result.py similarity index 100% rename from src/sevm/commands/result.py rename to src/chasm_debugger/commands/result.py diff --git a/src/sevm/commands/symbols.py b/src/chasm_debugger/commands/symbols.py similarity index 99% rename from src/sevm/commands/symbols.py rename to src/chasm_debugger/commands/symbols.py index a3a8825..0bb114e 100644 --- a/src/sevm/commands/symbols.py +++ b/src/chasm_debugger/commands/symbols.py @@ -79,7 +79,7 @@ def info_address(proc: CommandProcessor, args: list[str]) -> CommandResult: The target is a name, a canonical signature or a selector, optionally qualified as `Contract.name`. A signature no ABI here declares is hashed, as `cast sig` does, so - a selector can be traced through code sevm never compiled. + a selector can be traced through code chasm never compiled. """ contract, target = _split_contract("".join(args)) art, code = _address_code(proc, contract, target) diff --git a/src/sevm/compile/__init__.py b/src/chasm_debugger/compile/__init__.py similarity index 100% rename from src/sevm/compile/__init__.py rename to src/chasm_debugger/compile/__init__.py diff --git a/src/sevm/compile/build.py b/src/chasm_debugger/compile/build.py similarity index 100% rename from src/sevm/compile/build.py rename to src/chasm_debugger/compile/build.py diff --git a/src/sevm/compile/foundry_config.py b/src/chasm_debugger/compile/foundry_config.py similarity index 96% rename from src/sevm/compile/foundry_config.py rename to src/chasm_debugger/compile/foundry_config.py index 86c5565..fab1804 100644 --- a/src/sevm/compile/foundry_config.py +++ b/src/chasm_debugger/compile/foundry_config.py @@ -35,7 +35,7 @@ test = "test" libs = ["lib"] -# sevm forces the optimizer OFF for debuggable builds regardless of this file. +# chasm forces the optimizer OFF for debuggable builds regardless of this file. """ # For a lone .sol outside any project: no src/test layout to declare, just somewhere to @@ -44,7 +44,7 @@ [profile.default] libs = ["lib"] -# sevm forces the optimizer OFF for debuggable builds regardless of this file. +# chasm forces the optimizer OFF for debuggable builds regardless of this file. """ @@ -188,7 +188,7 @@ def _collect_project_sources( def _install_hint(import_path: str, importer: str) -> str: """Said when installs are off, whether by --no-install or by a declined prompt.""" return ( - f"unresolved import {import_path!r} in {importer}, and sevm was told not to " + f"unresolved import {import_path!r} in {importer}, and chasm was told not to " f"install it. Run again with -y to let it, or install it yourself:\n" f" forge install /\n" f" echo '{package_of(import_path)}/=lib//src/' >> remappings.txt" @@ -274,7 +274,7 @@ def unresolved_prefixes( ) -> list[str]: """Library prefixes the sources import but cannot reach on disk. - Reads files only, so `sevm run` can say what it would install before compiling. + Reads files only, so `chasm run` can say what it would install before compiling. """ cfg = cfg or read_foundry_config(root) sources = _collect_project_sources(root, (), source_dirs, cfg.libs) diff --git a/src/sevm/compile/model.py b/src/chasm_debugger/compile/model.py similarity index 99% rename from src/sevm/compile/model.py rename to src/chasm_debugger/compile/model.py index ec3f7c9..6af6df1 100644 --- a/src/sevm/compile/model.py +++ b/src/chasm_debugger/compile/model.py @@ -1,4 +1,4 @@ -"""The artifact model: what a compiled project looks like to the rest of sevm. +"""The artifact model: what a compiled project looks like to the rest of Chasm. Everything downstream depends only on these dataclasses, so a Foundry `out/*.json` importer could be added later by producing the same shapes. diff --git a/src/sevm/compile/solc.py b/src/chasm_debugger/compile/solc.py similarity index 100% rename from src/sevm/compile/solc.py rename to src/chasm_debugger/compile/solc.py diff --git a/src/sevm/compile/solcbin.py b/src/chasm_debugger/compile/solcbin.py similarity index 95% rename from src/sevm/compile/solcbin.py rename to src/chasm_debugger/compile/solcbin.py index 4ea9b8c..6e28c45 100644 --- a/src/sevm/compile/solcbin.py +++ b/src/chasm_debugger/compile/solcbin.py @@ -3,7 +3,7 @@ py-solc-x downloads from `binaries.soliditylang.org/{linux,macosx,windows}-amd64` and detects the OS only — there is no architecture check anywhere in it. On arm64 Linux or Apple silicon it therefore fetches an x86-64 binary and dies validating it, which is why -sevm ran on x86_64 alone. Provisioning happens here instead; py-solc-x still *invokes* +chasm ran on x86_64 alone. Provisioning happens here instead; py-solc-x still *invokes* solc (`compile_standard(solc_binary=...)`), which is the part of it that works. The platform map follows svm, Foundry's version manager, because it is the one @@ -69,7 +69,7 @@ _MACOS_UNIVERSAL = Version("0.8.24") _TIMEOUT = 30 -_HEADERS = {"User-Agent": "sevm"} +_HEADERS = {"User-Agent": "chasm"} # Set by `--solc-binary`, and read before anything is looked up or downloaded: on a # platform with no published build, a compiler the user already has is the whole answer. @@ -132,8 +132,8 @@ def use_binary(path: str | None) -> None: def override() -> str | None: - """The compiler the user pointed sevm at, if any.""" - return _override or os.environ.get("SEVM_SOLC") + """The compiler the user pointed chasm at, if any.""" + return _override or os.environ.get("CHASM_SOLC") def override_version() -> str | None: @@ -150,7 +150,7 @@ def platform_key() -> str: """This machine, named the way solc's own download directories are. `unsupported` means no published build exists (32-bit x86, armv7, FreeBSD, ...), not - that sevm is broken there: `SEVM_SOLC` still points it at a compiler. + that chasm is broken there: `CHASM_SOLC` still points it at a compiler. """ arm = platform.machine().lower() in ("aarch64", "arm64", "armv8b", "armv8l") if sys.platform.startswith("linux"): @@ -326,8 +326,8 @@ def version_of(path: str) -> str | None: def find(version: str) -> str | None: """An already-usable solc of `version`, or None. Never downloads. - `--solc-binary` and `SEVM_SOLC` override everything, including the version, because - pointing sevm at a compiler is the escape hatch for a machine no download works on. + `--solc-binary` and `CHASM_SOLC` override everything, including the version, because + pointing chasm at a compiler is the escape hatch for a machine no download works on. """ chosen = override() if chosen: @@ -435,7 +435,7 @@ def _fetch_wasm_index() -> dict[str, list[str]]: def wasm_dir() -> str: - """soljson.js bundles live in sevm's cache, not `~/.solcx`, which holds binaries.""" + """soljson.js bundles live in chasm's cache, not `~/.solcx`, which holds binaries.""" return os.path.join(cache.user_cache_dir(), "solc-wasm") @@ -476,7 +476,7 @@ def compiler(version: str) -> Compiler: if wasm.runtime() is None: raise CompileError( f"{native}\n\nsolc also has a WebAssembly build that runs anywhere, but " - "it needs a JS runtime: install node (or name one with SEVM_NODE)." + "it needs a JS runtime: install node (or name one with CHASM_NODE)." ) from native try: return Compiler(version, ensure_wasm(version), wasm=True) @@ -488,8 +488,8 @@ def _no_build_message(version: str, key: str) -> str: if key == "unsupported": return ( f"no solc {version} build is published for {platform.system()} " - f"{platform.machine()}. Point sevm at a compiler you built or installed " - "yourself with SEVM_SOLC=/path/to/solc." + f"{platform.machine()}. Point chasm at a compiler you built or installed " + "yourself with CHASM_SOLC=/path/to/solc." ) known = release_index(key) detail = "the release list could not be fetched" if not known else "no such release" @@ -498,7 +498,7 @@ def _no_build_message(version: str, key: str) -> str: extra = " (arm64 Linux builds start at 0.5.0)" return ( f"no solc {version} for {key}: {detail}{extra}. Pick a version that exists with " - "--solc, or set SEVM_SOLC=/path/to/solc." + "--solc, or set CHASM_SOLC=/path/to/solc." ) @@ -507,5 +507,5 @@ def _will_not_run_message(version: str, path: str) -> str: f"solc {version} downloaded to {path} but will not run on this machine. The " "official Linux builds are dynamically linked against glibc, so musl systems " "(Alpine) need gcompat and NixOS needs a wrapped solc. Install one yourself and " - "set SEVM_SOLC=/path/to/solc." + "set CHASM_SOLC=/path/to/solc." ) diff --git a/src/sevm/compile/soljson.js b/src/chasm_debugger/compile/soljson.js similarity index 93% rename from src/sevm/compile/soljson.js rename to src/chasm_debugger/compile/soljson.js index 0dcd76a..5bf4bee 100644 --- a/src/sevm/compile/soljson.js +++ b/src/chasm_debugger/compile/soljson.js @@ -3,7 +3,7 @@ // soljson.js is a ~9MB JS bundle with the wasm inlined, not a WASI module, so a JS // runtime is the only way to call it — there is no pure-Python route. It exposes solc's // C entry point, which the solc-js package wraps and which is stable from 0.6: -// `solidity_compile(input, readCallback, context)`. sevm inlines every source in the +// `solidity_compile(input, readCallback, context)`. chasm inlines every source in the // standard-JSON input, so the import callback is never needed and is passed as null. // // Usage: node soljson.js < input.json > output.json diff --git a/src/sevm/compile/versions.py b/src/chasm_debugger/compile/versions.py similarity index 100% rename from src/sevm/compile/versions.py rename to src/chasm_debugger/compile/versions.py diff --git a/src/sevm/compile/wasm.py b/src/chasm_debugger/compile/wasm.py similarity index 93% rename from src/sevm/compile/wasm.py rename to src/chasm_debugger/compile/wasm.py index 5968503..e08218d 100644 --- a/src/sevm/compile/wasm.py +++ b/src/chasm_debugger/compile/wasm.py @@ -6,8 +6,8 @@ the build cache stays valid across the two. It costs a JS runtime: soljson.js is an Emscripten bundle, not a WASI module, so wasmtime -and friends cannot load it and there is no pure-Python option. `node` is what sevm looks -for (`SEVM_NODE` names another). Loading the ~9MB bundle and compiling a small contract +and friends cannot load it and there is no pure-Python option. `node` is what chasm looks +for (`CHASM_NODE` names another). Loading the ~9MB bundle and compiling a small contract takes ~0.4s end to end against ~0.01s native, which is the reason this is a fallback and not the default. """ @@ -29,7 +29,7 @@ def runtime() -> str | None: """The JS runtime to drive soljson.js with, or None if the machine has none.""" - named = os.environ.get("SEVM_NODE") + named = os.environ.get("CHASM_NODE") candidates = (named,) if named else _RUNTIMES for candidate in candidates: found = shutil.which(candidate) @@ -49,7 +49,7 @@ def compile_standard(soljson: str, payload: dict) -> dict: if node is None: raise CompileError( "no JS runtime to run solc's WebAssembly build with. Install node, or name " - "one with SEVM_NODE, or point sevm at a native compiler with SEVM_SOLC." + "one with CHASM_NODE, or point chasm at a native compiler with CHASM_SOLC." ) done = subprocess.run( [node, DRIVER, soljson], @@ -69,7 +69,7 @@ def compile_standard(soljson: str, payload: dict) -> dict: # Solidity gives its builtins negative ids (`msg` is -15), and the Emscripten build -# serialises them as unsigned 32-bit: `referencedDeclaration: 4294967281`. Nothing sevm +# serialises them as unsigned 32-bit: `referencedDeclaration: 4294967281`. Nothing chasm # reads resolves a builtin, but normalising keeps the two backends' documents # interchangeable, which the build cache assumes — it is keyed on the compiler version, # never on which backend produced the entry. diff --git a/src/sevm/console.py b/src/chasm_debugger/console.py similarity index 95% rename from src/sevm/console.py rename to src/chasm_debugger/console.py index 44dafb0..f45328f 100644 --- a/src/sevm/console.py +++ b/src/chasm_debugger/console.py @@ -1,6 +1,6 @@ """The plain-text frontend. -Exists so the engine can be proved correct without a UI in the way, and so `sevm run +Exists so the engine can be proved correct without a UI in the way, and so `chasm run --console` works over ssh, in CI, and anywhere the TUI cannot draw. It is the same CommandProcessor the TUI drives, so behaviour cannot drift between the two. """ @@ -13,7 +13,7 @@ from .evaluate import Evaluator from .session import DebugSession, Finished, Paused -BANNER = """[bold]sevm[/bold] - Solidity/EVM debugger. [dim]gdb commands; `help` for the list.[/dim]""" +BANNER = """[bold]Chasm[/bold] - Solidity/EVM debugger. [dim]gdb commands; `help` for the list.[/dim]""" class ConsoleFrontend: @@ -69,7 +69,7 @@ def run(self, first_event=None) -> None: self.on_first_stop(first_event) while True: try: - raw = self.console.input("[bold green](sevm)[/bold green] ") + raw = self.console.input("[bold green](chasm)[/bold green] ") except (EOFError, KeyboardInterrupt): self.console.print() break diff --git a/src/sevm/decode.py b/src/chasm_debugger/decode.py similarity index 100% rename from src/sevm/decode.py rename to src/chasm_debugger/decode.py diff --git a/src/sevm/disasm.py b/src/chasm_debugger/disasm.py similarity index 100% rename from src/sevm/disasm.py rename to src/chasm_debugger/disasm.py diff --git a/src/sevm/dispatch.py b/src/chasm_debugger/dispatch.py similarity index 100% rename from src/sevm/dispatch.py rename to src/chasm_debugger/dispatch.py diff --git a/src/sevm/doctor.py b/src/chasm_debugger/doctor.py similarity index 91% rename from src/sevm/doctor.py rename to src/chasm_debugger/doctor.py index a5bb361..8c8ed65 100644 --- a/src/sevm/doctor.py +++ b/src/chasm_debugger/doctor.py @@ -1,4 +1,4 @@ -"""What sevm found on this machine, and what it would do about it. +"""What chasm found on this machine, and what it would do about it. Every question this answers came from a real support round: which build of solc this platform gets, whether the one already installed runs, whether the WebAssembly fallback @@ -23,7 +23,7 @@ @dataclass(frozen=True) class Line: - """One reported fact. `problem` marks the ones that stop sevm working.""" + """One reported fact. `problem` marks the ones that stop chasm working.""" label: str value: str @@ -65,7 +65,7 @@ def _solc_lines(version: str, node: str | None) -> list[Line]: lines.append( Line( "solc", - f"{override} (SEVM_SOLC or --solc-binary)" + f"{override} (CHASM_SOLC or --solc-binary)" + (f", reports {reported}" if reported else ", DOES NOT RUN"), problem=reported is None, ) @@ -102,13 +102,13 @@ def _solc_lines(version: str, node: str | None) -> list[Line]: def report() -> list[Line]: - """Everything `sevm doctor` prints, in order.""" + """Everything `chasm doctor` prints, in order.""" node = wasm.runtime() node_version = _tool_version(node, "--version") if node else None git = _tool_version("git", "--version") lines = [ - Line("sevm", _sevm_version()), + Line("chasm", _chasm_version()), Line( "python", f"{platform.python_version()} ({platform.python_implementation()}) " @@ -133,7 +133,7 @@ def report() -> list[Line]: overrides = [ f"{name}={os.environ[name]}" - for name in ("SEVM_SOLC", "SEVM_NODE", "SOLCX_BINARY_PATH", "SEVM_NO_CACHE") + for name in ("CHASM_SOLC", "CHASM_NODE", "SOLCX_BINARY_PATH", "CHASM_NO_CACHE") if os.environ.get(name) ] if overrides: @@ -141,11 +141,11 @@ def report() -> list[Line]: return lines -def _sevm_version() -> str: +def _chasm_version() -> str: from importlib.metadata import PackageNotFoundError, version try: - return version("sevm") + return version("chasm") except PackageNotFoundError: # pragma: no cover - running from a source tree return "unknown" diff --git a/src/sevm/evaluate/__init__.py b/src/chasm_debugger/evaluate/__init__.py similarity index 92% rename from src/sevm/evaluate/__init__.py rename to src/chasm_debugger/evaluate/__init__.py index 0a46592..1e7bcd4 100644 --- a/src/sevm/evaluate/__init__.py +++ b/src/chasm_debugger/evaluate/__init__.py @@ -1,7 +1,7 @@ """Solidity expression evaluation at a breakpoint. The trick is to borrow the real Solidity compiler rather than write an interpreter: splice -`function __sevm_eval() returns (T) { return (); }` into the paused contract's +`function __chasm_eval() returns (T) { return (); }` into the paused contract's source, compile it, swap the runtime code in on a state snapshot, run it, decode, revert. bindings.py getting locals and `msg.data`/`msg.sig` into the injected function diff --git a/src/sevm/evaluate/bindings.py b/src/chasm_debugger/evaluate/bindings.py similarity index 97% rename from src/sevm/evaluate/bindings.py rename to src/chasm_debugger/evaluate/bindings.py index e656e85..f54e075 100644 --- a/src/sevm/evaluate/bindings.py +++ b/src/chasm_debugger/evaluate/bindings.py @@ -21,15 +21,15 @@ from ..locals import referenced_names -EVAL_FUNCTION = "__sevm_eval" +EVAL_FUNCTION = "__chasm_eval" EVAL_SELECTOR = function_signature_to_4byte_selector(f"{EVAL_FUNCTION}()") # msg field -> (parameter name, declared type, abi type). `bytes calldata` rather than # `bytes memory` so `msg.data[4:]` still slices, which is how anyone reads arguments out # of raw calldata. MSG_FIELDS: dict[str, tuple[str, str, str]] = { - "data": ("__sevm_msg_data", "bytes calldata", "bytes"), - "sig": ("__sevm_msg_sig", "bytes4", "bytes4"), + "data": ("__chasm_msg_data", "bytes calldata", "bytes"), + "sig": ("__chasm_msg_sig", "bytes4", "bytes4"), } _MSG_FIELD = re.compile(r"\bmsg\s*\.\s*(data|sig)\b") diff --git a/src/sevm/evaluate/evaluator.py b/src/chasm_debugger/evaluate/evaluator.py similarity index 99% rename from src/sevm/evaluate/evaluator.py rename to src/chasm_debugger/evaluate/evaluator.py index c21ae70..960231f 100644 --- a/src/sevm/evaluate/evaluator.py +++ b/src/chasm_debugger/evaluate/evaluator.py @@ -2,7 +2,7 @@ Borrow the real Solidity compiler instead of writing an interpreter. Splice - function __sevm_eval() public payable returns (T) { return (); } + function __chasm_eval() public payable returns (T) { return (); } into the paused contract's source, compile it, swap the runtime code at the target address on a state snapshot, run it, decode the output, revert. Operator precedence, checked @@ -159,7 +159,7 @@ def _known_type( `p amount` and `p totalDeposits` are most of what gets typed at a prompt, and typing them costs a whole compile of its own. This is a lookup rather than a - second type checker: a bound local resolves to the parameter sevm itself + second type checker: a bound local resolves to the parameter chasm itself declares, and a state variable to solc's own `typeString`, so both are the string the probe's diagnostic would have reported. Anything else returns None and goes to solc. diff --git a/src/sevm/evaluate/injection.py b/src/chasm_debugger/evaluate/injection.py similarity index 97% rename from src/sevm/evaluate/injection.py rename to src/chasm_debugger/evaluate/injection.py index 618d466..b543853 100644 --- a/src/sevm/evaluate/injection.py +++ b/src/chasm_debugger/evaluate/injection.py @@ -11,10 +11,10 @@ from dataclasses import dataclass from typing import Any -EVAL_FUNCTION = "__sevm_eval" +EVAL_FUNCTION = "__chasm_eval" -PROBE_STRUCT = "__SevmProbe" -PROBE_DECL = f"struct {PROBE_STRUCT} {{ uint8 __sevm_x; }}" +PROBE_STRUCT = "__ChasmProbe" +PROBE_DECL = f"struct {PROBE_STRUCT} {{ uint8 __chasm_x; }}" PROBE_TYPE = f"{PROBE_STRUCT} memory" # solc phrases the mismatch two different ways depending on where it is caught. diff --git a/src/sevm/foundry.py b/src/chasm_debugger/foundry.py similarity index 97% rename from src/sevm/foundry.py rename to src/chasm_debugger/foundry.py index 6f83e45..3c88adb 100644 --- a/src/sevm/foundry.py +++ b/src/chasm_debugger/foundry.py @@ -28,11 +28,11 @@ class TestTarget: @dataclass(frozen=True) class Prepared: - """What `sevm run` may do to the target directory, after asking.""" + """What `chasm run` may do to the target directory, after asking.""" root: str existing: bool # a foundry.toml was already there - may_install: bool # the user let sevm fetch missing libraries + may_install: bool # the user let chasm fetch missing libraries declined: bool = False # asked, and the answer was no (or there was no terminal) missing: tuple[str, ...] = () # libraries that would have been installed @@ -113,7 +113,7 @@ def prepare_project( approved = assume_yes if not approved and sys.stdin.isatty(): - prompt = "sevm will:\n" + "".join(f" - {p}\n" for p in planned) + "[y/N] " + prompt = "chasm will:\n" + "".join(f" - {p}\n" for p in planned) + "[y/N] " try: approved = input(prompt).strip().lower() in ("y", "yes") except EOFError: @@ -220,7 +220,7 @@ def select_test( class TestFailed(RuntimeError): - """A test transaction reverted. forge would print a failure; sevm ends the run with one.""" + """A test transaction reverted. forge would print a failure; chasm ends the run with one.""" @dataclass(frozen=True) diff --git a/src/sevm/frames.py b/src/chasm_debugger/frames.py similarity index 100% rename from src/sevm/frames.py rename to src/chasm_debugger/frames.py diff --git a/src/sevm/libs.py b/src/chasm_debugger/libs.py similarity index 98% rename from src/sevm/libs.py rename to src/chasm_debugger/libs.py index 91e664c..b5ea650 100644 --- a/src/sevm/libs.py +++ b/src/chasm_debugger/libs.py @@ -1,9 +1,9 @@ """Solidity dependency resolution: read imports, find the repo, clone it, remap it. -sevm no longer ships its own forge-std. A `.t.sol` (or any contract) gets the real library +chasm no longer ships its own forge-std. A `.t.sol` (or any contract) gets the real library from the real source, installed the way `forge install` leaves it: a shallow clone under `lib/`, pinned to the newest stable tag, with an explicit remapping so `forge` and -sevm resolve the import identically. +chasm resolve the import identically. Git is the only requirement: `git ls-remote --tags` lists releases unauthenticated (no GitHub API rate limit) and `git clone --depth 1 --branch ` fetches one. The `forge` @@ -187,7 +187,7 @@ def _run_git(args: Sequence[str], timeout: float = _GIT_TIMEOUT) -> str: ) except FileNotFoundError as exc: raise LibError( - "git is not installed; sevm needs it to fetch dependencies" + "git is not installed; chasm needs it to fetch dependencies" ) from exc except subprocess.TimeoutExpired as exc: raise LibError(f"git {' '.join(args)} timed out") from exc @@ -354,7 +354,7 @@ def install( if repo_url is None: raise LibError( f"cannot resolve import {sample_import!r}: no library named {prefix!r} in " - f"sevm's table or on npm.\n" + f"chasm's table or on npm.\n" f"Install it yourself and add the remapping:\n" f" forge install /\n" f" echo '{prefix}/=lib//src/' >> remappings.txt" @@ -397,7 +397,7 @@ def missing_prefixes(unresolved: Iterable[str]) -> list[str]: def write_remappings(root: str, entries: Sequence[str]) -> list[str]: - """Append missing lines to `/remappings.txt` so `forge` resolves as sevm does. + """Append missing lines to `/remappings.txt` so `forge` resolves as chasm does. Returns the lines actually added; existing entries are never rewritten or reordered. """ diff --git a/src/sevm/locals/__init__.py b/src/chasm_debugger/locals/__init__.py similarity index 100% rename from src/sevm/locals/__init__.py rename to src/chasm_debugger/locals/__init__.py diff --git a/src/sevm/locals/index.py b/src/chasm_debugger/locals/index.py similarity index 100% rename from src/sevm/locals/index.py rename to src/chasm_debugger/locals/index.py diff --git a/src/sevm/locals/layout.py b/src/chasm_debugger/locals/layout.py similarity index 100% rename from src/sevm/locals/layout.py rename to src/chasm_debugger/locals/layout.py diff --git a/src/sevm/locals/values.py b/src/chasm_debugger/locals/values.py similarity index 100% rename from src/sevm/locals/values.py rename to src/chasm_debugger/locals/values.py diff --git a/src/sevm/mcp_server/__init__.py b/src/chasm_debugger/mcp_server/__init__.py similarity index 86% rename from src/sevm/mcp_server/__init__.py rename to src/chasm_debugger/mcp_server/__init__.py index a3b652a..6f6563f 100644 --- a/src/sevm/mcp_server/__init__.py +++ b/src/chasm_debugger/mcp_server/__init__.py @@ -3,11 +3,11 @@ Where the console renders Rich markup and the TUI draws panes, this frontend returns structured JSON with explicit windows and truncation flags — an LLM cannot scroll a pane or watch colour, but it can narrow an offset. Launch with -`sevm mcp` (stdio transport). +`chasm mcp` (stdio transport). Where things live: - driver.py `DebugDriver`: owns the one session, mirrors `sevm run`'s wiring, + driver.py `DebugDriver`: owns the one session, mirrors `chasm run`'s wiring, and remembers the previous stop for diffs serialize.py value formatting: hex words, memory windows, the stop report tools.py the tool registrations over the driver diff --git a/src/sevm/mcp_server/driver.py b/src/chasm_debugger/mcp_server/driver.py similarity index 98% rename from src/sevm/mcp_server/driver.py rename to src/chasm_debugger/mcp_server/driver.py index 5faf529..7cd195e 100644 --- a/src/sevm/mcp_server/driver.py +++ b/src/chasm_debugger/mcp_server/driver.py @@ -3,7 +3,7 @@ The console and the TUI are thin renderers over `CommandProcessor`; this is the third frontend, and the only thing it draws is JSON. `DebugDriver` owns one debug session (single active target, replaced by the next start), mirrors the -wiring `sevm run` does for both target kinds, and remembers the previous stop so +wiring `chasm run` does for both target kinds, and remembers the previous stop so it can answer "what changed?" — the question a human answers by watching panes. """ @@ -65,7 +65,7 @@ def _notice(self, message: str) -> None: def _require(self) -> tuple[DebugSession, CommandProcessor]: if self._session is None or self._processor is None: raise DriverError( - "no active debug session; call sevm_start_session first " + "no active debug session; call chasm_start_session first " "(script_path for a web3.py driver or a .t.sol Foundry test)" ) return self._session, self._processor @@ -86,7 +86,7 @@ def start( """Compile and start a debug target; replaces any existing session. `provenance=true` records every executed opcode from the start, enabling - `sevm_why_stack` and `sevm_export_trace` without a second pass. + `chasm_why_stack` and `chasm_export_trace` without a second pass. `reference_runtime_hex` (the deployed runtime, as an explorer or `cast code` prints it) hard-fails the start when the compiled contract diverges beyond the metadata tail: pcs and gadget offsets silently @@ -134,7 +134,7 @@ def _start_script( solc: str | None, optimize: bool, ) -> dict[str, Any]: - # Same wiring as `sevm run