├── docs ├── requirements.txt ├── toc.rst ├── make.bat ├── conf.py ├── index.rst ├── utils.rst ├── getstarted.rst ├── evm.rst └── contract.rst ├── simular ├── __init__.py ├── utils.py ├── simular.pyi └── contract.py ├── src ├── core │ ├── mod.rs │ ├── snapshot.rs │ ├── errors.rs │ ├── fork.rs │ ├── in_memory_db.rs │ ├── fork_backend.rs │ ├── storage.rs │ ├── abi.rs │ └── evm.rs ├── lib.rs ├── pyabi.rs └── pyevm.rs ├── Makefile ├── tests ├── fixtures │ ├── contracts │ │ ├── MockERC20.sol │ │ └── KitchenSink.sol │ ├── erc20.abi │ ├── SignedInts.json │ ├── BlockMeta.json │ └── erc20.bin ├── test_datatypes.py ├── conftest.py ├── test_pyabi.py ├── test_pyevm.py └── test_contract.py ├── .gitignore ├── .readthedocs.yaml ├── bench └── simple.py ├── .github └── workflows │ ├── test_pypi.yml │ ├── mdbook.yml │ └── build-publish.yml ├── pyproject.toml ├── README.md ├── Cargo.toml └── LICENSE /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx==7.2.6 2 | sphinx_rtd_theme==2.0.0 -------------------------------------------------------------------------------- /docs/toc.rst: -------------------------------------------------------------------------------- 1 | .. toctree:: 2 | :maxdepth: 2 3 | 4 | getstarted 5 | evm 6 | contract 7 | utils -------------------------------------------------------------------------------- /simular/__init__.py: -------------------------------------------------------------------------------- 1 | from .simular import PyEvm, PyAbi, TxResult 2 | from .contract import Contract 3 | from .utils import * 4 | -------------------------------------------------------------------------------- /src/core/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod abi; 2 | pub mod evm; 3 | 4 | pub mod errors; 5 | pub mod fork; 6 | pub mod fork_backend; 7 | pub mod in_memory_db; 8 | pub mod snapshot; 9 | pub mod storage; 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: docs 2 | 3 | build: 4 | hatch run dev:maturin develop 5 | 6 | # this builds the production release 7 | prod_release: 8 | hatch run dev:maturin build 9 | 10 | test: build 11 | hatch run dev:pytest tests/* 12 | 13 | rust_tests: 14 | cargo test --no-default-features 15 | 16 | benchit: prod_release 17 | hatch run dev:python bench/simple.py 18 | 19 | docs: 20 | hatch run dev:docs 21 | 22 | shell: 23 | hatch --env dev shell 24 | -------------------------------------------------------------------------------- /tests/fixtures/contracts/MockERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import {ERC20} from "solmate/tokens/ERC20.sol"; 5 | 6 | contract MockERC20 is ERC20 { 7 | address public owner; 8 | 9 | constructor( 10 | string memory _name, 11 | string memory _symbol, 12 | uint8 _decimals 13 | ) ERC20(_name, _symbol, _decimals) { 14 | owner = msg.sender; 15 | } 16 | 17 | function mint(address to, uint256 value) public virtual { 18 | require(msg.sender == owner, "not the owner"); 19 | _mint(to, value); 20 | } 21 | 22 | function burn(address from, uint256 value) public virtual { 23 | require(msg.sender == owner, "not the owner"); 24 | _burn(from, value); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /target 3 | /Cargo.lock 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | .pytest_cache/ 8 | *.py[cod] 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | .venv/ 16 | env/ 17 | bin/ 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | include/ 28 | man/ 29 | venv/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | pip-selfcheck.json 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | 47 | # Translations 48 | *.mo 49 | 50 | # VSCode 51 | .vscode/ 52 | 53 | # Pyenv 54 | .python-version 55 | 56 | # Forkdb script 57 | tests/dump_usdc.py 58 | 59 | # Mdbook 60 | docs/book 61 | 62 | todo.md 63 | 64 | uniswap/ 65 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod core; 2 | mod pyabi; 3 | mod pyevm; 4 | 5 | use alloy_primitives::Address; 6 | use anyhow::Result; 7 | use pyo3::prelude::*; 8 | 9 | /// Convert strings to addresses. String addresses are passed through from Python. 10 | pub fn str_to_address(caller: &str) -> Result
{ 11 | let c = caller 12 | .parse::
() 13 | .map_err(|_| anyhow::anyhow!("failed to parse caller address from string"))?; 14 | Ok(c) 15 | } 16 | 17 | #[pymodule] 18 | fn simular(m: &Bound<'_, PyModule>) -> PyResult<()> { 19 | m.add_class::()?; 20 | m.add_class::()?; 21 | m.add_class::()?; 22 | Ok(()) 23 | } 24 | 25 | /* 26 | #[pymodule] 27 | fn simular(_py: Python<'_>, m: &PyModule) -> PyResult<()> { 28 | m.add_class::()?; 29 | m.add_class::()?; 30 | m.add_class::()?; 31 | Ok(()) 32 | }*/ 33 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the OS, Python version and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.12" 13 | # You can also specify other tool versions: 14 | # nodejs: "19" 15 | # rust: "1.64" 16 | # golang: "1.19" 17 | 18 | # Build documentation in the "docs/" directory with Sphinx 19 | sphinx: 20 | configuration: docs/conf.py 21 | 22 | # Optionally build your docs in additional formats such as PDF and ePub 23 | # formats: 24 | # - pdf 25 | # - epub 26 | 27 | # Optional but recommended, declare the Python requirements required 28 | # to build your documentation 29 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 30 | python: 31 | install: 32 | - requirements: docs/requirements.txt -------------------------------------------------------------------------------- /bench/simple.py: -------------------------------------------------------------------------------- 1 | import time 2 | from pathlib import Path 3 | 4 | from simular import PyEvm, create_account, contract_from_raw_abi, ether_to_wei 5 | 6 | PATH = Path(__file__).parent 7 | NUM_TX = 150_000 8 | 9 | 10 | def how_fast(): 11 | with open(f"{PATH}/../tests/fixtures/KitchenSink.json") as f: 12 | abi = f.read() 13 | 14 | client = PyEvm() 15 | deployer = create_account(client, value=ether_to_wei(2)) 16 | 17 | counter = contract_from_raw_abi(client, abi) 18 | counter.deploy(caller=deployer) 19 | 20 | start_time = time.perf_counter() 21 | 22 | for _ in range(0, NUM_TX): 23 | counter.increment.transact(caller=deployer) 24 | 25 | end_time = time.perf_counter() 26 | total_time = end_time - start_time 27 | 28 | val = counter.value.call() 29 | assert NUM_TX == val 30 | print(f"{NUM_TX} transactions in : {total_time:.6f} second(s)") 31 | 32 | 33 | if __name__ == "__main__": 34 | how_fast() 35 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /tests/fixtures/contracts/KitchenSink.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | pragma solidity ^0.8.13; 3 | 4 | struct InputStruct { 5 | uint256 x; 6 | uint256 y; 7 | address user; 8 | } 9 | 10 | contract KitchenSink { 11 | uint256 public value; 12 | uint256 public x; 13 | uint256 public y; 14 | address public user; 15 | 16 | // increment by 1 17 | function increment() public returns (uint256) { 18 | value += 1; 19 | return value; 20 | } 21 | 22 | // increment by 'input' 23 | function increment(uint256 input) public returns (uint256, uint256) { 24 | value += input; 25 | return (input, value); 26 | } 27 | 28 | // set values by input struct 29 | function setInput(InputStruct calldata input) public returns (address) { 30 | x = input.x; 31 | y = input.y; 32 | user = input.user; 33 | return user; 34 | } 35 | 36 | receive() external payable {} 37 | } 38 | -------------------------------------------------------------------------------- /src/core/snapshot.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Containers for serializing EVM state information 3 | //! 4 | use revm::primitives::{Address, Bytes, U256}; 5 | use serde::{Deserialize, Serialize}; 6 | use std::collections::BTreeMap; 7 | 8 | /// Source of the snapshop. Either from a fork or the local in-memory database. 9 | #[derive(Clone, Debug, Serialize, Deserialize, Default)] 10 | pub enum SnapShotSource { 11 | Memory, 12 | #[default] 13 | Fork, 14 | } 15 | 16 | /// A single AccountRecord and it's associated storage. `SnapShot` stores 17 | /// a map of Accounts. 18 | #[derive(Clone, Debug, Serialize, Deserialize)] 19 | pub struct SnapShotAccountRecord { 20 | pub nonce: u64, 21 | pub balance: U256, 22 | pub code: Bytes, 23 | pub storage: BTreeMap, 24 | } 25 | 26 | /// The high-level objects containing all the snapshot information. 27 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] 28 | pub struct SnapShot { 29 | pub source: SnapShotSource, 30 | pub block_num: u64, 31 | pub timestamp: u64, 32 | pub accounts: BTreeMap, 33 | } 34 | -------------------------------------------------------------------------------- /tests/test_datatypes.py: -------------------------------------------------------------------------------- 1 | from simular import ( 2 | contract_from_raw_abi, 3 | create_account, 4 | ) 5 | 6 | 7 | def test_signed_ints(evm, alice, signed_ints_json): 8 | create_account(evm, alice) 9 | test_contract = contract_from_raw_abi(evm, signed_ints_json) 10 | assert test_contract.deploy(caller=alice) 11 | 12 | # values 13 | cases = [ 14 | (-128, 127), 15 | (-549755813888, 549755813887), 16 | (-2361183241434822606848, 2361183241434822606847), 17 | ( 18 | -43556142965880123323311949751266331066368, 19 | 43556142965880123323311949751266331066367, 20 | ), 21 | ( 22 | -3138550867693340381917894711603833208051177722232017256448, 23 | 3138550867693340381917894711603833208051177722232017256447, 24 | ), 25 | ( 26 | -57896044618658097711785492504343953926634992332820282019728792003956564819968, 27 | 57896044618658097711785492504343953926634992332820282019728792003956564819967, 28 | ), 29 | ] 30 | 31 | for n, p in cases: 32 | assert n == test_contract.in_and_out.call(n) 33 | assert p == test_contract.in_and_out.call(p) 34 | -------------------------------------------------------------------------------- /src/core/errors.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Database errors 3 | //! 4 | use alloy_primitives::{Address, U256}; 5 | use revm::primitives::EVMError; 6 | use revm::primitives::B256; 7 | use thiserror::Error; 8 | 9 | use std::convert::Infallible; 10 | 11 | /// Wrapper for Database errors 12 | #[derive(Error, Debug)] 13 | pub enum DatabaseError { 14 | //#[error("missing AccountInfo {0}")] 15 | //MissingAccount(Address), 16 | #[error("code should already be loaded: {0}")] 17 | MissingCode(B256), 18 | #[error("failed to get account for {0}")] 19 | GetAccount(Address), 20 | #[error("failed to get storage for {0} at {1}")] 21 | GetStorage(Address, U256), 22 | #[error("failed to get block hash for {0}")] 23 | GetBlockHash(U256), 24 | #[error("{0}")] 25 | Other(String), 26 | } 27 | 28 | impl From> for DatabaseError { 29 | fn from(err: EVMError) -> Self { 30 | match err { 31 | EVMError::Database(err) => err, 32 | err => DatabaseError::Other(err.to_string()), 33 | } 34 | } 35 | } 36 | 37 | impl From for DatabaseError { 38 | fn from(value: Infallible) -> Self { 39 | match value {} 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | 7 | # -- Project information ----------------------------------------------------- 8 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 9 | 10 | project = "simular" 11 | copyright = "2024, The MITRE Corporation" 12 | author = "Dave Bryson" 13 | 14 | # -- General configuration --------------------------------------------------- 15 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 16 | 17 | extensions = [ 18 | "sphinx.ext.intersphinx", 19 | "sphinx.ext.doctest", 20 | "sphinx.ext.coverage", 21 | "sphinx.ext.viewcode", 22 | "sphinx_rtd_theme", 23 | ] 24 | 25 | add_module_names = False 26 | 27 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 28 | 29 | # templates_path = ["_templates"] 30 | 31 | # -- Options for HTML output ------------------------------------------------- 32 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 33 | 34 | html_theme = "sphinx_rtd_theme" 35 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pathlib import Path 3 | 4 | from simular import PyEvm 5 | 6 | PATH = Path(__file__).parent 7 | 8 | 9 | @pytest.fixture 10 | def bob(): 11 | return "0xed6ff00ae6a64df0bf28e159c4a48311b931f458" 12 | 13 | 14 | @pytest.fixture 15 | def alice(): 16 | return "0x0091410228bf6062ab28c949ba4172ee9144bfde" 17 | 18 | 19 | @pytest.fixture 20 | def evm(): 21 | return PyEvm() 22 | 23 | 24 | @pytest.fixture 25 | def erc20abi(): 26 | with open(f"{PATH}/./fixtures/erc20.abi") as f: 27 | ercabi = f.read() 28 | return ercabi 29 | 30 | 31 | @pytest.fixture 32 | def erc20bin(): 33 | with open(f"{PATH}/./fixtures/erc20.bin") as f: 34 | ercbin = f.read() 35 | bits = bytes.fromhex(ercbin) 36 | return bits 37 | 38 | 39 | @pytest.fixture 40 | def kitchen_sink_json(): 41 | with open(f"{PATH}/./fixtures/KitchenSink.json") as f: 42 | rawabi = f.read() 43 | return rawabi 44 | 45 | 46 | @pytest.fixture 47 | def block_meta_json(): 48 | with open(f"{PATH}/./fixtures/BlockMeta.json") as f: 49 | rawabi = f.read() 50 | return rawabi 51 | 52 | 53 | @pytest.fixture 54 | def signed_ints_json(): 55 | with open(f"{PATH}/./fixtures/SignedInts.json") as f: 56 | rawabi = f.read() 57 | return rawabi 58 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. _index: 2 | 3 | simular 4 | ======= 5 | 6 | **Simular** is a Python API wrapped around a production grade Ethereum Virtual Machine (EVM). You can use to 7 | locally deploy and interact with smart contracts, create accounts, transfer Ether, and much more. 8 | 9 | How is it different than Brownie, Ganache, Anvil? 10 | 11 | - It's only an EVM. 12 | - No HTTP/JSON-RPC. You talk directly to the EVM (and it's fast) 13 | - Full functionality: account transfers, contract interaction, and more. 14 | 15 | The primary motivation for this work is to have a lightweight, fast environment for simulating and modeling Ethereum applications. 16 | 17 | Features 18 | -------- 19 | 20 | - User-friendy Python API 21 | - Run a local version with an in-memory database. Or copy (fork) state from a remote node. 22 | - Parse Solidity ABI json files or define a specific set of functions using `human-readable` notation. 23 | - Dump the current state of the EVM to json for future use in pre-populating EVM storage. 24 | 25 | 26 | Standing on the shoulders of giants... 27 | -------------------------------------- 28 | 29 | Thanks to the following projects for making this work possible! 30 | 31 | - `pyO3 `_ 32 | - `revm `_ 33 | - `alloy-rs `_ 34 | - `eth_utils/eth_abi `_ 35 | 36 | 37 | .. include:: toc.rst 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /.github/workflows/test_pypi.yml: -------------------------------------------------------------------------------- 1 | name: test_pypi 2 | 3 | on: 4 | workflow_dispatch 5 | 6 | jobs: 7 | check_main: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Release from main 11 | if: github.ref == 'refs/heads/master' 12 | run: echo '::notice ::Building and uploading release' 13 | - name: No release 14 | if: github.ref != 'refs/heads/master' 15 | run: echo '::warning ::Release can only be run from the main branch!' && exit 1 16 | 17 | sdist: 18 | runs-on: ubuntu-latest 19 | needs: [check_main] 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Build sdist 23 | uses: PyO3/maturin-action@v1 24 | with: 25 | command: sdist 26 | args: --out dist 27 | - name: Upload sdist 28 | uses: actions/upload-artifact@v4 29 | with: 30 | name: wheels-sdist 31 | path: dist 32 | 33 | publish_test_pypi: 34 | runs-on: ubuntu-latest 35 | needs: [sdist] 36 | environment: 37 | name: testpypi 38 | url: https://test.pypi.org/p/simular-evm 39 | permissions: 40 | id-token: write 41 | steps: 42 | - name: Download dists 43 | uses: actions/download-artifact@v4 44 | with: 45 | pattern: wheels-* 46 | merge-multiple: true 47 | path: dist/ 48 | - name: Publish distribution 📦 to PyPI 49 | uses: pypa/gh-action-pypi-publish@v1.8.14 50 | with: 51 | repository-url: https://test.pypi.org/legacy/ 52 | 53 | 54 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin==1.7.4"] 3 | build-backend = "maturin" 4 | 5 | [tool.maturin] 6 | features = ["pyo3/extension-module"] 7 | sdist-include = ["LICENSE", "README.md"] 8 | 9 | 10 | [project] 11 | name = "simular-evm" 12 | version = "0.3.0" 13 | requires-python = ">=3.10,<3.12" 14 | authors = [ 15 | { name = "Dave Bryson", email = "davebryson@users.noreply.github.com" }, 16 | ] 17 | license = "Apache-2.0" 18 | readme = "README.md" 19 | keywords = ["agent-based modeling", "ethereum", "solidity", "simulation"] 20 | description = "smart-contract api and embedded ethereum virtual machine" 21 | classifiers = [ 22 | "Programming Language :: Python", 23 | "Programming Language :: Python :: 3.10", 24 | "Programming Language :: Python :: 3.11", 25 | "Programming Language :: Rust", 26 | "Programming Language :: Python :: Implementation :: CPython", 27 | "Programming Language :: Python :: Implementation :: PyPy", 28 | ] 29 | 30 | dependencies = ["eth-abi>=4.1.0", "eth-utils>=2.2.0"] 31 | 32 | [tool.hatch.envs.dev] 33 | dependencies = [ 34 | "pytest>=7.4.0", 35 | "black>=23.7.0", 36 | "maturin==1.7.4", 37 | "sphinx>=7.2.6", 38 | "sphinx_rtd_theme>=2.0.0", 39 | ] 40 | 41 | [tool.hatch.envs.dev.scripts] 42 | docs = "sphinx-build -M html docs docs/build" 43 | 44 | [project.urls] 45 | homepage = "https://github.com/simular-fi/simular" 46 | repository = "https://github.com/simular-fi/simular" 47 | documentation = "https://simular.readthedocs.io/en/latest/" 48 | issues = "https://github.com/simular-fi/simular/issues" 49 | -------------------------------------------------------------------------------- /.github/workflows/mdbook.yml: -------------------------------------------------------------------------------- 1 | # Workflow for building and deploying a mdBook site to GitHub Pages 2 | # 3 | # To get started with mdBook see: https://rust-lang.github.io/mdBook/index.html 4 | # 5 | name: Deploy mdBook site to Pages 6 | 7 | on: 8 | # Allows you to run this workflow manually from the Actions tab 9 | workflow_dispatch: 10 | 11 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 12 | permissions: 13 | contents: read 14 | pages: write 15 | id-token: write 16 | 17 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 18 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 19 | concurrency: 20 | group: "pages" 21 | cancel-in-progress: false 22 | 23 | jobs: 24 | # Build job 25 | build: 26 | runs-on: ubuntu-latest 27 | env: 28 | MDBOOK_VERSION: 0.4.36 29 | steps: 30 | - uses: actions/checkout@v4 31 | - name: Install mdBook 32 | run: | 33 | curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf -y | sh 34 | rustup update 35 | cargo install --version ${MDBOOK_VERSION} mdbook 36 | - name: Setup Pages 37 | id: pages 38 | uses: actions/configure-pages@v4 39 | - name: Build with mdBook 40 | run: mdbook build docs/ 41 | - name: Upload artifact 42 | uses: actions/upload-pages-artifact@v3 43 | with: 44 | path: ./docs/book 45 | 46 | # Deployment job 47 | deploy: 48 | environment: 49 | name: github-pages 50 | url: ${{ steps.deployment.outputs.page_url }} 51 | runs-on: ubuntu-latest 52 | needs: build 53 | steps: 54 | - name: Deploy to GitHub Pages 55 | id: deployment 56 | uses: actions/deploy-pages@v4 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Simular 3 | 4 | [![pypi](https://img.shields.io/pypi/v/simular-evm.svg)](https://pypi.python.org/pypi/simular-evm) 5 | 6 | A Python smart-contract API with a fast (embedded) Ethereum Virtual Machine (EVM). `Simular` creates a Python wrapper around production grade Rust based Ethereum APIs. 7 | 8 | How is it different than Brownie, Ganache, Anvil? 9 | - It's only an EVM, no blocks or mining 10 | - No HTTP/JSON-RPC. You talk directly to the EVM (and it's fast) 11 | - Full functionality: account transfers, contract interaction, etc... 12 | 13 | The primary motivation for this work is to be able to model smart-contract interaction in an Agent Based Modeling environment like [Mesa](https://mesa.readthedocs.io/en/main/). 14 | 15 | ## Features 16 | - `EVM`: run a local version with an in-memory database, or fork db state from a remote node. 17 | - `Snapshot`: dump the current state of the EVM to json for future use in pre-populating EVM storage 18 | - `ABI`: parse compiled Solidity json files or define a specific set of functions using `human-readable` notation 19 | - `Contract`: high-level, user-friendy Python API 20 | 21 | ## Build from source 22 | - You need `Rust` and `Python`, and optionally `Make`. We use `hatch` for Python project management, but it's not required 23 | - Create a local Python virtual environment. Within that environment install Python dependencies 24 | - Run `make build` or `hatch run maturin develop` 25 | - See `simular/` for the main python api 26 | 27 | ## Getting Started 28 | See [Simular Documentation](https://simular.readthedocs.io/en/latest/) for examples and API details. 29 | 30 | ## Standing on the shoulders of giants... 31 | Thanks to the following projects for making this work possible! 32 | - [pyO3](https://github.com/PyO3) 33 | - [revm](https://github.com/bluealloy/revm) 34 | - [alloy-rs](https://github.com/alloy-rs) 35 | - [eth_utils/eth_abi](https://eth-utils.readthedocs.io/en/stable/) 36 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "simular" 3 | version = "0.3.0" 4 | edition = "2021" 5 | authors = ["Dave Bryson"] 6 | readme = "README.md" 7 | license = "Apache-2.0" 8 | description = "smart-contract api and embedded ethereum virtual machine" 9 | repository = "https://github.com/simular-fi/simular" 10 | homepage = "https://github.com/simular-fi/simular" 11 | categories = ["cryptography::cryptocurrencies"] 12 | keywords = ["revm", "python", "ethereum", "web3", "abm"] 13 | rust-version = "1.76.0" 14 | 15 | [lib] 16 | name = "simular" 17 | crate-type = ["cdylib", "rlib"] 18 | 19 | 20 | [dependencies] 21 | # helpers 22 | anyhow = "1.0.81" 23 | thiserror = "1.0.58" 24 | serde = "1.0.165" 25 | serde_json = "1.0.99" 26 | hex = { version = "0.4.3", features = ["serde"] } 27 | 28 | # Alloy 29 | alloy-dyn-abi = "0.7.0" 30 | alloy-json-abi = "0.7.0" 31 | alloy-primitives = { version = "0.7.0", default-features = false } 32 | alloy-sol-types = { version = "0.7.0", features = ["json"] } 33 | 34 | # EVM 35 | revm = { version = "8.0.0", default-features = false, features = [ 36 | "tokio", 37 | "memory_limit", 38 | "optional_eip3607", 39 | "optional_block_gas_limit", 40 | "optional_no_base_fee", 41 | "arbitrary", 42 | ] } 43 | 44 | 45 | # required for forkdb 46 | tokio = { version = "1.37.0", features = ["rt-multi-thread", "macros"] } 47 | ethers-core = { version = "2.0.10", default-features = false } 48 | ethers-providers = "2.0.10" 49 | # need this feature in reqwest to deal with potential self-signed certs 50 | reqwest = { version = "0.11.19", features = ["rustls-tls"] } 51 | # resolve build issues on Ubuntu 52 | openssl = { version = "0.10", features = ["vendored"] } 53 | 54 | # Python wrapper 55 | pyo3 = { version = "0.24.1", features = ["multiple-pymethods", "anyhow"] } 56 | 57 | 58 | # using this to allow cargo test on rust code. See: 59 | # https://github.com/PyO3/pyo3/issues/340#issuecomment-461514532 60 | [features] 61 | extension-module = ["pyo3/extension-module"] 62 | default = ["extension-module"] 63 | -------------------------------------------------------------------------------- /tests/test_pyabi.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from simular import PyAbi 4 | from simular.contract import convert_for_soltypes 5 | 6 | 7 | def test_load_from_parts(erc20abi, erc20bin): 8 | abi = PyAbi.from_abi_bytecode(erc20abi, erc20bin) 9 | assert abi.has_function("mint") 10 | assert abi.has_function("burn") 11 | assert not abi.has_fallback() 12 | assert not abi.has_receive() 13 | 14 | addy = "0xa32f31673577f7a717716d8b88d85a9e7bbb76d3" 15 | (sig1, _, _) = abi.encode_function("mint", f"({addy}, 2)") 16 | hexed = bytes.hex(bytes(sig1)) 17 | og = "40c10f19000000000000000000000000a32f31673577f7a717716d8b88d85a9e7bbb76d30000000000000000000000000000000000000000000000000000000000000002" 18 | assert og == hexed 19 | 20 | (sig2, _, _) = abi.encode_function("name", "()") 21 | hexed2 = bytes.hex(bytes(sig2)) 22 | assert "06fdde03" == hexed2 23 | 24 | 25 | def test_load_from_human_readable(): 26 | funcs = ["function mint(address, uint256)", "function name()(string)"] 27 | abi = PyAbi.from_human_readable(funcs) 28 | assert abi.has_function("mint") 29 | assert abi.has_function("name") 30 | 31 | 32 | def test_load_full_json_abi(kitchen_sink_json): 33 | abi = PyAbi.from_full_json(kitchen_sink_json) 34 | assert abi.has_function("increment") 35 | assert abi.has_function("setInput") 36 | assert abi.has_receive() 37 | assert (abi.bytecode(), False) == abi.encode_constructor("()") 38 | 39 | with_struct = "fa6a38d200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003000000000000000000000000a32f31673577f7a717716d8b88d85a9e7bbb76d3" 40 | (sig3, _, _) = abi.encode_function( 41 | "setInput", "((2, 3, 0xa32f31673577f7a717716d8b88d85a9e7bbb76d3))" 42 | ) 43 | hexed3 = bytes.hex(bytes(sig3)) 44 | assert with_struct == hexed3 45 | 46 | 47 | def test_convert_to_soltypes(): 48 | with pytest.raises(BaseException): 49 | convert_for_soltypes(1) 50 | 51 | assert "(1)" == convert_for_soltypes((1,)) 52 | assert "(1, (2, (3, false)))" == convert_for_soltypes((1, (2, (3, False)))) 53 | assert "(1, dave, true, 0x0)" == convert_for_soltypes((1, "dave", True, "0x0")) 54 | assert "((1, 2), (false, bob))" == convert_for_soltypes(((1, 2), (False, "bob"))) 55 | -------------------------------------------------------------------------------- /tests/test_pyevm.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from eth_utils import to_wei 3 | from eth_abi import decode 4 | 5 | from simular import PyEvm, PyAbi, contract_from_raw_abi 6 | 7 | 8 | def test_create_account_and_balance(evm, bob): 9 | two_ether = to_wei(2, "ether") 10 | 11 | assert evm.get_balance(bob) == 0 12 | evm.create_account(bob, two_ether) 13 | assert evm.get_balance(bob) == two_ether 14 | 15 | 16 | def test_transfer_and_dump_state(evm, bob, alice): 17 | one_ether = to_wei(1, "ether") 18 | two_ether = to_wei(2, "ether") 19 | 20 | evm.create_account(bob, two_ether) 21 | assert evm.get_balance(bob) == two_ether 22 | 23 | evm.transfer(bob, alice, one_ether) 24 | 25 | assert evm.get_balance(bob) == one_ether 26 | assert evm.get_balance(alice) == one_ether 27 | 28 | with pytest.raises(BaseException): 29 | # bob doesn't have enough... 30 | evm.transfer(bob, alice, two_ether) 31 | 32 | assert evm.get_balance(bob) == one_ether 33 | assert evm.get_balance(alice) == one_ether 34 | 35 | # dump and reload state... 36 | state = evm.create_snapshot() 37 | evm2 = PyEvm.from_snapshot(state) 38 | 39 | assert evm2.get_balance(bob) == one_ether 40 | assert evm2.get_balance(alice) == one_ether 41 | 42 | 43 | def test_contract_raw_interaction(evm, bob, kitchen_sink_json): 44 | abi = PyAbi.from_full_json(kitchen_sink_json) 45 | bytecode = abi.bytecode() 46 | 47 | contract_address = evm.deploy("()", bob, 0, abi) 48 | (enc, _, _) = abi.encode_function("increment", "()") 49 | 50 | with pytest.raises(BaseException): 51 | evm.transact(bob, "Ox01", enc, 0) 52 | 53 | evm.transact("increment", "()", bob, contract_address, 0, abi) 54 | 55 | (enc1, _, _) = abi.encode_function("value", "()") 56 | assert 1 == evm.call("value", "()", contract_address, abi) 57 | 58 | 59 | def test_advance_block(evm, bob, block_meta_json): 60 | # simple contract that can return block.timestamp and number 61 | evm.create_account(bob) 62 | 63 | contract = contract_from_raw_abi(evm, block_meta_json) 64 | contract.deploy(caller=bob) 65 | 66 | [ts1, bn1] = contract.getMeta.call() 67 | 68 | assert bn1 == 1 # start at block 1 69 | 70 | evm.advance_block() 71 | evm.advance_block() 72 | evm.advance_block() 73 | 74 | [ts2, bn2] = contract.getMeta.call() 75 | 76 | assert bn2 == 4 # block advanced 77 | assert ts2 == ts1 + 36 # timestamp advanced 78 | -------------------------------------------------------------------------------- /tests/test_contract.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from simular import ( 4 | contract_from_abi_bytecode, 5 | contract_from_raw_abi, 6 | create_account, 7 | PyEvm, 8 | contract_from_inline_abi, 9 | ether_to_wei, 10 | ) 11 | 12 | 13 | def test_loading_contracts(evm): 14 | with pytest.raises(BaseException): 15 | contract_from_raw_abi(evm, "") 16 | 17 | with pytest.raises(BaseException): 18 | contract_from_raw_abi(evm, {}) 19 | 20 | with pytest.raises(BaseException): 21 | contract_from_abi_bytecode(evm, "", b"") 22 | 23 | 24 | def test_contract_interface(evm, bob, alice, erc20abi, erc20bin): 25 | create_account(evm, alice, 0) 26 | create_account(evm, bob, 0) 27 | 28 | erc20 = contract_from_abi_bytecode(evm, erc20abi, erc20bin) 29 | 30 | erc20.deploy("USD Coin", "USDC", 6, caller=bob) 31 | contract_address = erc20.address 32 | 33 | assert erc20.name.call() == "USD Coin" 34 | assert erc20.decimals.call() == 6 35 | assert erc20.owner.call() == bob 36 | 37 | txr = erc20.mint.transact(alice, 10, caller=bob) 38 | assert txr.event["Transfer"] 39 | (_, to, amt) = txr.event["Transfer"] 40 | assert to == alice 41 | assert amt == 10 42 | 43 | assert txr.gas_used > 0 44 | 45 | assert 10 == erc20.balanceOf.call(alice) 46 | assert 10 == erc20.totalSupply.call() 47 | 48 | with pytest.raises(BaseException): 49 | # alice can't mint, she's not the owner! 50 | erc20.mint.transact(alice, 10, caller=alice) 51 | 52 | # Test state 53 | evm2 = PyEvm.from_snapshot(evm.create_snapshot()) 54 | 55 | erc20again = contract_from_inline_abi(evm2, ["function totalSupply() (uint256)"]) 56 | erc20again.at(contract_address) 57 | assert 10 == erc20again.totalSupply.call() 58 | 59 | 60 | def test_deploy_and_test_kitchensink(evm, alice, kitchen_sink_json): 61 | create_account(evm, alice, ether_to_wei(2)) 62 | a = contract_from_raw_abi(evm, kitchen_sink_json) 63 | 64 | # fail on value with a non-payable constructor 65 | with pytest.raises(BaseException): 66 | a.deploy(caller=alice, value=1) 67 | 68 | assert a.deploy(caller=alice) 69 | 70 | assert 1 == a.increment.transact(caller=alice).output 71 | assert [2, 3] == a.increment.transact(2, caller=alice).output 72 | assert 4 == a.increment.simulate(caller=alice).output 73 | assert 3 == a.value.call() 74 | assert alice == a.setInput.transact((1, 2, alice), caller=alice).output 75 | 76 | # receive 77 | one_ether = ether_to_wei(1) 78 | assert 0 == evm.get_balance(a.address) 79 | evm.transfer(alice, a.address, one_ether) 80 | assert one_ether == evm.get_balance(alice) 81 | assert one_ether == evm.get_balance(a.address) 82 | -------------------------------------------------------------------------------- /src/pyabi.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Python wrapper for `simular-core::ContractAbi` 3 | //! 4 | use alloy_dyn_abi::DynSolType; 5 | use pyo3::{prelude::*, pybacked::PyBackedStr}; 6 | use std::str; 7 | 8 | use crate::core::abi::ContractAbi; 9 | 10 | /// Can load and parse ABI information. Used in `Contract.py` to 11 | /// process function calls. 12 | #[pyclass] 13 | pub struct PyAbi(pub ContractAbi); 14 | 15 | #[pymethods] 16 | impl PyAbi { 17 | /// Load a complete ABI file from a compiled Solidity contract. 18 | /// This is a raw un-parsed json file that includes both `abi` and `bytecode`. 19 | #[staticmethod] 20 | pub fn from_full_json(abi: &str) -> Self { 21 | Self(ContractAbi::from_full_json(abi)) 22 | } 23 | 24 | /// Load from the un-parsed json `abi` and optionally `bytecode` 25 | #[staticmethod] 26 | pub fn from_abi_bytecode(abi: &str, bytes: Option>) -> Self { 27 | Self(ContractAbi::from_abi_bytecode(abi, bytes)) 28 | } 29 | 30 | /// Create an ABI by providing shortened definitions of the functions 31 | /// of interest. 32 | /// 33 | /// ## Example: 34 | /// 35 | /// `["function hello() (uint256)"]` creates the function `hello` that 36 | /// can be encoded/decoded for calls to the Evm. 37 | #[staticmethod] 38 | pub fn from_human_readable(values: Vec) -> Self { 39 | // convert PyBackStr into &str 40 | let mapped: Vec<&str> = values 41 | .iter() 42 | .map(|i| str::from_utf8(i.as_bytes()).unwrap()) 43 | .collect(); 44 | Self(ContractAbi::from_human_readable(mapped)) 45 | } 46 | 47 | /// Does the ABI contain the function `name` 48 | pub fn has_function(&self, name: &str) -> bool { 49 | self.0.has_function(name) 50 | } 51 | 52 | /// Does the Contract have a fallback function? 53 | pub fn has_fallback(&self) -> bool { 54 | self.0.has_fallback() 55 | } 56 | 57 | /// Does the contract have a receive function? 58 | pub fn has_receive(&self) -> bool { 59 | self.0.has_receive() 60 | } 61 | 62 | /// Return the contract bytecode 63 | pub fn bytecode(&self) -> Option> { 64 | self.0.bytecode() 65 | } 66 | 67 | /// Encode constructor arguments. 68 | /// Returns the encoded args, and whether the constructor is payable 69 | pub fn encode_constructor(&self, args: &str) -> anyhow::Result<(Vec, bool)> { 70 | self.0.encode_constructor(args) 71 | } 72 | 73 | /// Encode the arguments for a specific function. 74 | /// Returns: 75 | /// - `encoded args` 76 | /// - `is the function payable?` 77 | /// - `DynSolType` to decode output from function 78 | pub fn encode_function( 79 | &self, 80 | name: &str, 81 | args: &str, 82 | ) -> anyhow::Result<(Vec, bool, DynSolTypeWrapper)> { 83 | let (enc, is_payable, dt) = self.0.encode_function(name, args).unwrap(); 84 | Ok((enc, is_payable, DynSolTypeWrapper(dt))) 85 | } 86 | } 87 | 88 | /// Wrapper needed by PyO3 for DynSolType 89 | #[pyclass] 90 | pub struct DynSolTypeWrapper(pub Option); 91 | -------------------------------------------------------------------------------- /tests/fixtures/erc20.abi: -------------------------------------------------------------------------------- 1 | [{"type":"constructor","inputs":[{"name":"_name","type":"string","internalType":"string"},{"name":"_symbol","type":"string","internalType":"string"},{"name":"_decimals","type":"uint8","internalType":"uint8"}],"stateMutability":"nonpayable"},{"type":"function","name":"DOMAIN_SEPARATOR","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"allowance","inputs":[{"name":"","type":"address","internalType":"address"},{"name":"","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"approve","inputs":[{"name":"spender","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"nonpayable"},{"type":"function","name":"balanceOf","inputs":[{"name":"","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"burn","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"value","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"decimals","inputs":[],"outputs":[{"name":"","type":"uint8","internalType":"uint8"}],"stateMutability":"view"},{"type":"function","name":"mint","inputs":[{"name":"to","type":"address","internalType":"address"},{"name":"value","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"name","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"nonces","inputs":[{"name":"","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"owner","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"permit","inputs":[{"name":"owner","type":"address","internalType":"address"},{"name":"spender","type":"address","internalType":"address"},{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"symbol","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"totalSupply","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"transfer","inputs":[{"name":"to","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"nonpayable"},{"type":"function","name":"transferFrom","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"to","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"nonpayable"},{"type":"event","name":"Approval","inputs":[{"name":"owner","type":"address","indexed":true,"internalType":"address"},{"name":"spender","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Transfer","inputs":[{"name":"from","type":"address","indexed":true,"internalType":"address"},{"name":"to","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false}] 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/utils.rst: -------------------------------------------------------------------------------- 1 | .. _utils: 2 | 3 | Utilities 4 | ========= 5 | 6 | .. contents:: :local: 7 | 8 | .. py:module:: simular.utils 9 | 10 | Provides several ``helper`` functions to simplify working with ``accounts`` and ``Contract`` 11 | 12 | Functions 13 | --------- 14 | 15 | .. py:function:: ether_to_wei(value: int) -> int 16 | 17 | Convert Ether to Wei 18 | 19 | :params value: the given value in Ether to convert 20 | :return: the amount in Wei 21 | 22 | .. py:function:: generate_random_address() -> str 23 | 24 | Randomly generate an Ethereum address 25 | 26 | :return: (str) the hex-encoded address 27 | 28 | .. py:function:: create_account(evm: PyEvm, address: str = None, value: int = 0) -> str 29 | 30 | Create a new account in the Evm 31 | 32 | :param evm: an instance of PyEvm 33 | :param address: (optional) a valid, hex-encoded Ethereum address 34 | :param balance: (optional) balance in ``wei`` to set for the account. 35 | :return: the hex-encoded address 36 | 37 | .. note:: 38 | * If no address is provided, a random address will be created 39 | * If no value is provided, the account balance will be set to 0 40 | 41 | 42 | .. py:function:: create_many_accounts(evm: PyEvm, num: int, value: int = 0) -> [str] 43 | 44 | Just like ``create_account`` except it can create many accounts at once. 45 | 46 | :param evm: an instance of PyEvm 47 | :param num: the number of accounts to create 48 | :param value: (optional) the initial balance in `wei` for each account 49 | :return: a list of account addresses 50 | 51 | 52 | .. py:function:: contract_from_raw_abi(evm: PyEvm, raw_abi: str) -> Contract 53 | 54 | Create an instance of ``Contract`` given the full ABI. A full ABI should include 55 | the `abi` and `bytecode`. This is usually a single json file from a compiled Solidity contract. 56 | 57 | :param evm: an instance of PyEvm 58 | :param raw_abi: the full abi contents 59 | :return: an instance of ``Contract`` based on the ABI and contract creation bytecode 60 | 61 | .. note:: 62 | Don't forget to ``deploy`` the contract to make it available in the EVM 63 | 64 | 65 | .. py:function:: contract_from_abi_bytecode(evm: PyEvm, raw_abi: str, bytecode: bytes = None) -> Contract 66 | 67 | Create an instance of ``Contract`` from the ABI and (optionally) the contract creation 68 | bytecode. This is often used when you have the ABI and bytecode are not in the same file OR 69 | when you just want to create a ``Contract`` using just the ABI to interact with an already 70 | deployed contract. 71 | 72 | :param evm: an instance of PyEvm 73 | :param raw_abi: the full abi contents 74 | :param bytecode: (optional) contract creation code 75 | 76 | :return: an instance of ``Contract`` 77 | 78 | .. note:: 79 | Don't forget to ``deploy`` the contract to make it available in the EVM 80 | 81 | 82 | .. py:function:: contract_from_inline_abi(evm: PyEvm, abi: typing.List[str]) -> Contract 83 | 84 | Create an instance of ``Contract`` from a user-friendly form of the ABI This is used 85 | to interact with an already deployed contract. See `Human-Friendly ABI `_ 86 | 87 | :param evm: an instance of PyEvm 88 | :param abi: a list of (str) describing the contract's functions 89 | :param bytecode: (optional) contract creation code 90 | 91 | :return: an instance of ``Contract`` 92 | 93 | .. warning:: 94 | The contract must already be deployed. You will need to use ``Contract.at()`` to 95 | set the address of the contract. 96 | 97 | Example: 98 | 99 | .. code-block:: python 100 | 101 | >>> evm = PyEvm() 102 | 103 | # specifies a single contract function 'hello' 104 | # that takes no arguments and returns a number 105 | >>> abi = ["function hello()(uint256)"] 106 | 107 | >>> contract = contract_from_inline_abi(abi) 108 | >>> contract.at('deployed contracts address') 109 | 110 | # call it 111 | >>> value = contract.hello.call() 112 | 113 | -------------------------------------------------------------------------------- /.github/workflows/build-publish.yml: -------------------------------------------------------------------------------- 1 | name: build-publish 2 | 3 | on: 4 | workflow_dispatch 5 | 6 | jobs: 7 | check_main: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Release from main 11 | if: github.ref == 'refs/heads/master' 12 | run: echo '::notice ::Building and uploading release' 13 | - name: No release 14 | if: github.ref != 'refs/heads/master' 15 | run: echo '::warning ::Release can only be run from the master branch!' && exit 1 16 | 17 | macos: 18 | runs-on: macos-latest 19 | strategy: 20 | matrix: 21 | target: [x86_64, aarch64] 22 | needs: [check_main] 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: actions/setup-python@v5 26 | with: 27 | python-version: 3.x 28 | - name: Build wheels 29 | uses: PyO3/maturin-action@v1 30 | with: 31 | target: ${{ matrix.target }} 32 | args: --release --out dist --find-interpreter 33 | sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} 34 | - name: Upload wheels 35 | uses: actions/upload-artifact@v4 36 | with: 37 | name: wheels-macos-${{ matrix.target }} 38 | path: dist 39 | 40 | linux: 41 | runs-on: ubuntu-latest 42 | strategy: 43 | matrix: 44 | target: [x86_64, aarch64, armv7] 45 | needs: [check_main] 46 | steps: 47 | - uses: actions/checkout@v4 48 | - uses: actions/setup-python@v5 49 | with: 50 | python-version: 3.x 51 | - name: Build wheels 52 | uses: PyO3/maturin-action@v1 53 | with: 54 | target: ${{ matrix.target }} 55 | args: --release --out dist --find-interpreter 56 | sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} 57 | manylinux: manylinux_2_28 58 | container: "ghcr.io/rust-cross/manylinux_2_28-cross:${{ matrix.target }}" 59 | before-script-linux: | 60 | sudo apt-get update 61 | sudo apt-get install --yes --upgrade build-essential cmake protobuf-compiler libssl-dev glibc-source musl-tools 62 | - name: Upload wheels 63 | uses: actions/upload-artifact@v4 64 | with: 65 | name: wheels-linux-${{ matrix.target }} 66 | path: dist 67 | 68 | windows: 69 | runs-on: windows-latest 70 | strategy: 71 | matrix: 72 | target: [x64, x86] 73 | needs: [check_main] 74 | steps: 75 | - uses: actions/checkout@v4 76 | - uses: actions/setup-python@v5 77 | with: 78 | python-version: 3.x 79 | - name: Build wheels 80 | uses: PyO3/maturin-action@v1 81 | with: 82 | target: ${{ matrix.target }} 83 | args: --release --out dist --find-interpreter 84 | sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} 85 | - name: Upload wheels 86 | uses: actions/upload-artifact@v4 87 | with: 88 | name: wheels-windows-${{ matrix.target }} 89 | path: dist 90 | 91 | sdist: 92 | runs-on: ubuntu-latest 93 | needs: [check_main] 94 | steps: 95 | - uses: actions/checkout@v4 96 | - uses: actions/setup-python@v5 97 | with: 98 | python-version: 3.x 99 | - name: Build sdist 100 | uses: PyO3/maturin-action@v1 101 | with: 102 | command: sdist 103 | args: --out dist 104 | - name: Upload sdist 105 | uses: actions/upload-artifact@v4 106 | with: 107 | name: wheels-sdist 108 | path: dist 109 | 110 | publish_pypi: 111 | runs-on: ubuntu-latest 112 | needs: [linux, windows, macos, sdist] 113 | environment: 114 | name: pypi 115 | url: https://pypi.org/p/simular-evm 116 | permissions: 117 | id-token: write 118 | steps: 119 | - name: Download dists 120 | uses: actions/download-artifact@v4 121 | with: 122 | pattern: wheels-* 123 | merge-multiple: true 124 | path: dist/ 125 | - name: Publish distribution 📦 to PyPI 126 | uses: pypa/gh-action-pypi-publish@v1.8.14 127 | -------------------------------------------------------------------------------- /tests/fixtures/SignedInts.json: -------------------------------------------------------------------------------- 1 | {"abi":[{"type":"function","name":"in_and_out","inputs":[{"name":"value","type":"int256","internalType":"int256"}],"outputs":[{"name":"","type":"int256","internalType":"int256"}],"stateMutability":"pure"}],"bytecode":{"object":"0x608060405234801561000f575f80fd5b5061010f8061001d5f395ff3fe6080604052348015600e575f80fd5b50600436106026575f3560e01c80636ee2e03314602a575b5f80fd5b60406004803603810190603c9190608f565b6054565b604051604b919060c2565b60405180910390f35b5f819050919050565b5f80fd5b5f819050919050565b6071816061565b8114607a575f80fd5b50565b5f81359050608981606a565b92915050565b5f6020828403121560a15760a0605d565b5b5f60ac84828501607d565b91505092915050565b60bc816061565b82525050565b5f60208201905060d35f83018460b5565b9291505056fea2646970667358221220689ef493bdd4f6c5f20643b97ad191ce1462d8cd83851a0c0ce36898ea4ecacf64736f6c63430008150033","sourceMap":"65:176:0:-:0;;;;;;;;;;;;;;;;;;;","linkReferences":{}},"deployedBytecode":{"object":"0x6080604052348015600e575f80fd5b50600436106026575f3560e01c80636ee2e03314602a575b5f80fd5b60406004803603810190603c9190608f565b6054565b604051604b919060c2565b60405180910390f35b5f819050919050565b5f80fd5b5f819050919050565b6071816061565b8114607a575f80fd5b50565b5f81359050608981606a565b92915050565b5f6020828403121560a15760a0605d565b5b5f60ac84828501607d565b91505092915050565b60bc816061565b82525050565b5f60208201905060d35f83018460b5565b9291505056fea2646970667358221220689ef493bdd4f6c5f20643b97ad191ce1462d8cd83851a0c0ce36898ea4ecacf64736f6c63430008150033","sourceMap":"65:176:0:-:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;147:92;;;;;;;;;;;;;:::i;:::-;;:::i;:::-;;;;;;;:::i;:::-;;;;;;;;;202:6;227:5;220:12;;147:92;;;:::o;88:117:1:-;197:1;194;187:12;334:76;370:7;399:5;388:16;;334:76;;;:::o;416:120::-;488:23;505:5;488:23;:::i;:::-;481:5;478:34;468:62;;526:1;523;516:12;468:62;416:120;:::o;542:137::-;587:5;625:6;612:20;603:29;;641:32;667:5;641:32;:::i;:::-;542:137;;;;:::o;685:327::-;743:6;792:2;780:9;771:7;767:23;763:32;760:119;;;798:79;;:::i;:::-;760:119;918:1;943:52;987:7;978:6;967:9;963:22;943:52;:::i;:::-;933:62;;889:116;685:327;;;;:::o;1018:115::-;1103:23;1120:5;1103:23;:::i;:::-;1098:3;1091:36;1018:115;;:::o;1139:218::-;1230:4;1268:2;1257:9;1253:18;1245:26;;1281:69;1347:1;1336:9;1332:17;1323:6;1281:69;:::i;:::-;1139:218;;;;:::o","linkReferences":{}},"methodIdentifiers":{"in_and_out(int256)":"6ee2e033"},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.21+commit.d9974bed\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[{\"internalType\":\"int256\",\"name\":\"value\",\"type\":\"int256\"}],\"name\":\"in_and_out\",\"outputs\":[{\"internalType\":\"int256\",\"name\":\"\",\"type\":\"int256\"}],\"stateMutability\":\"pure\",\"type\":\"function\"}],\"devdoc\":{\"kind\":\"dev\",\"methods\":{},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"src/SignedInts.sol\":\"SignedInts\"},\"evmVersion\":\"shanghai\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":false,\"runs\":200},\"remappings\":[\":ds-test/=lib/forge-std/lib/ds-test/src/\",\":forge-std/=lib/forge-std/src/\",\":solmate/=lib/solmate/src/\"]},\"sources\":{\"src/SignedInts.sol\":{\"keccak256\":\"0xbf0381daedc8cd8aa7dde82cb78c1cdf221e8eac0a2dd0adc0689804d6406c59\",\"license\":\"Apache-2.0\",\"urls\":[\"bzz-raw://a5e364031357a11f6676369eb79544b7f6c8e7c49809cd21069e90babec46f43\",\"dweb:/ipfs/QmWQUjM1w1JNPTdH1hduEXFB8deFso8Aqa8S9kQ1Wvq7u2\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.21+commit.d9974bed"},"language":"Solidity","output":{"abi":[{"inputs":[{"internalType":"int256","name":"value","type":"int256"}],"stateMutability":"pure","type":"function","name":"in_and_out","outputs":[{"internalType":"int256","name":"","type":"int256"}]}],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":["ds-test/=lib/forge-std/lib/ds-test/src/","forge-std/=lib/forge-std/src/","solmate/=lib/solmate/src/"],"optimizer":{"enabled":false,"runs":200},"metadata":{"bytecodeHash":"ipfs"},"compilationTarget":{"src/SignedInts.sol":"SignedInts"},"evmVersion":"shanghai","libraries":{}},"sources":{"src/SignedInts.sol":{"keccak256":"0xbf0381daedc8cd8aa7dde82cb78c1cdf221e8eac0a2dd0adc0689804d6406c59","urls":["bzz-raw://a5e364031357a11f6676369eb79544b7f6c8e7c49809cd21069e90babec46f43","dweb:/ipfs/QmWQUjM1w1JNPTdH1hduEXFB8deFso8Aqa8S9kQ1Wvq7u2"],"license":"Apache-2.0"}},"version":1},"id":0} -------------------------------------------------------------------------------- /docs/getstarted.rst: -------------------------------------------------------------------------------- 1 | .. _getstarted: 2 | 3 | Getting Started 4 | =============== 5 | 6 | Install 7 | ------- 8 | 9 | Simular is available on `PyPi `_ 10 | It requires Python ``>=3.10``. 11 | 12 | .. code-block:: bash 13 | 14 | >>> pip install simular-evm 15 | 16 | Here are a few examples of how to use simular. 17 | 18 | Transfer Ether 19 | -------------- 20 | 21 | Create 2 Ethereum accounts and transfer Ether between the accounts. 22 | 23 | .. code-block:: python 24 | 25 | # import the EVM and a few utility function 26 | >>> from simular inport PyEvm, ether_to_wei, create_account 27 | 28 | # create an instance of the Evm 29 | >>> evm = PyEvm() 30 | 31 | # convert 1 ether to wei (1e18) 32 | >>> one_ether = ether_to_wei(1) 33 | 34 | # create a couple of accounts - one for Bob with and initial 35 | # balance of 1 ether and one for Alice with no balance. 36 | >>> bob = create_account(evm, value=one_ether) 37 | >>> alice = create_account(evm) 38 | 39 | # Bob transfers 1 ether to alice 40 | >>> evm.transfer(bob, alice, one_ether) 41 | 42 | # check balances 43 | >>> assert int(1e18) == evm.get_balance(alice) 44 | >>> assert 0 == evm.get_balance(bob) 45 | 46 | 47 | Deploy and interact with a contract 48 | ----------------------------------- 49 | 50 | Load an ERC20 contract and mint tokens. 51 | 52 | .. code-block:: python 53 | 54 | # import the EVM and a few utility functions 55 | from simular inport PyEvm, create_account, contract_from_abi_bytecode 56 | 57 | def deploy_and_mint(): 58 | 59 | # Create an instance of the EVM 60 | evm = PyEvm() 61 | 62 | # Create accounts 63 | alice = create_account(evm) 64 | bob = create_account(evm) 65 | 66 | # Load the contract. 67 | # ABI and BYTECODE are the str versions of the ERC20 contract 68 | # interface and compiled bytecode 69 | erc20 = contract_from_abi_bytecode(evm, ABI, BYTECODE) 70 | 71 | # Deploy the contract. Returns the contract's address 72 | # The contract's constructor takes 3 arguments: 73 | # name: MyCoin 74 | # symbol: MYC 75 | # decimals: 6 76 | # 77 | # 'caller' (bob) is the one deploying the contract. This 78 | # translates to 'msg.sender'. And in the case of this contract, 79 | # bob will be the 'owner' 80 | contract_address = erc20.deploy("MyCoin", "MYC", 6, caller=bob) 81 | print(contract_address) 82 | 83 | 84 | # Let's check to see if it worked... 85 | # Notice how the contract functions are added as attributes 86 | # to the contract object. 87 | # 88 | # We use 'call' to make a read-only request 89 | assert erc20.name.call() == "MyCoin" 90 | assert erc20.decimals.call() == 6 91 | assert erc20.owner.call() == bob 92 | 93 | # Bob mints 10 tokens to alice. 94 | # Again, 'mint' is a contract function. It's 95 | # automatically attached to the erc20 contract 96 | # object as an attribute. 97 | # 'transact' is a write call to the contract (it will change state). 98 | tx1 = erc20.mint.transact(alice, 10, caller=bob) 99 | 100 | # View emitted events 101 | print(tx1.event['Transfer']) 102 | 103 | # check balances and supply 104 | assert 10 == erc20.balanceOf.call(alice) 105 | assert 10 == erc20.totalSupply.call() 106 | 107 | # Let's take a snapshot of the state of the EVM 108 | # and use it again later to pre-populate the EVM: 109 | snapshot = evm.create_snapshot() 110 | 111 | # and save it to a file 112 | with open('erc20snap.json', 'w') as f: 113 | f.write(snapshot) 114 | 115 | 116 | # ... later on, we can load this back into the EVM 117 | with open('erc20snap.json') as f: 118 | snapback = f.read() 119 | 120 | # a new instance of the EVM 121 | evm2 = PyEvm.from_snapshot(snapback) 122 | 123 | # load the contract definitions 124 | erc20back = contract_from_abi_bytecode(evm2, erc20abi, erc20bin) 125 | 126 | # check the state was preserved in the snapshot 127 | assert 10 == erc20back.balanceOf.call(alice) 128 | -------------------------------------------------------------------------------- /docs/evm.rst: -------------------------------------------------------------------------------- 1 | .. _evm: 2 | 3 | EVM 4 | === 5 | 6 | .. contents:: :local: 7 | 8 | 9 | .. py:module:: simular.PyEvm 10 | 11 | A Python wrapper around an Embedded `Rust based Ethereum Virtual Machine `_. 12 | 13 | Constructor 14 | ----------- 15 | 16 | .. py:class:: PyEvm() 17 | 18 | Create and return an instance of the EVM that uses an ``in-memory`` database. 19 | 20 | Example: 21 | 22 | .. code-block:: python 23 | 24 | >>> from simular import PyEvm 25 | >>> evm = PyEvm() 26 | 27 | 28 | Methods 29 | ------- 30 | 31 | .. py:staticmethod:: PyEvm.from_fork(url: str, blocknumber: int=None) 32 | 33 | Create and return an instance of the EVM that will pull state from a remote 34 | Ethereum node. 35 | 36 | :param url: the url (``https://...``) to a remote Ethereum node with JSON-RPC support 37 | :param blocknumber: (optional) the specific blocknumber to pull state at. If ``None``, the latest block will be used. 38 | :return: an instance of the EVM 39 | 40 | Example: 41 | 42 | .. code-block:: python 43 | 44 | >>> from simular import PyEvm 45 | >>> evm = PyEvm.from_fork('http://...', blocknumber=195653) 46 | 47 | 48 | .. py:staticmethod:: PyEvm.from_snapshot(snapshot: str) 49 | 50 | Create and return an instance of the EVM from a previously created ``snapshot``. 51 | See ``create_snapshot`` below. 52 | 53 | :param snapshot: a (str) serialized snapshot 54 | :return: an instance of the EVM 55 | 56 | Example: 57 | 58 | Assume ``hello_snap.json`` is a file from a saved snapshot. 59 | 60 | .. code-block:: python 61 | 62 | >>> from simular import PyEvm 63 | 64 | # load the json file 65 | >>> with open('hello_snap.json') as f: 66 | ... snap = f.read() 67 | >>> evm = PyEvm.from_snapshot(snap) 68 | 69 | 70 | .. py:method:: create_snapshot() 71 | 72 | Create a JSON formatted snapshot of the current state of the EVM. 73 | 74 | :return: (str) the serialized state 75 | 76 | Example: 77 | 78 | .. code-block:: python 79 | 80 | >>> evm = PyEvm() 81 | 82 | # do stuff with the EVM 83 | 84 | >>> snap = evm.create_snapshot() 85 | 86 | .. py:method:: create_account(address: str, balance = None) 87 | 88 | Create an account 89 | 90 | .. note:: 91 | See ``utils.create_account`` 92 | 93 | :param address: (str) a valid, hex-encoded Ethereum address 94 | :param balance: (int) and optional balance in ``wei`` to set for the account. If None, balance = 0 95 | 96 | 97 | .. py:method:: get_balance(address: str) 98 | 99 | Get the balance of the given address 100 | 101 | :param address: (str) a valid, hex-encoded Ethereum address 102 | :return: the balance in ``wei`` 103 | 104 | 105 | .. py:method:: transfer(caller: str, to: str, amount: int) 106 | 107 | Transfer ``amount`` in ``wei`` from ``caller -> to`` 108 | 109 | :param caller: (str) a valid, hex-encoded Ethereum address 110 | :param to: (str) a valid, hex-encoded Ethereum address 111 | :param amount: (int) the amount to transfer 112 | 113 | .. warning:: 114 | This will fail if the ``caller`` does not have a sufficient balance to transfer 115 | 116 | Example: 117 | 118 | .. code-block:: python 119 | 120 | >>> from similar import PyEvm, create_account 121 | 122 | >>> evm = PyEvm() 123 | 124 | # create an account for Bob with 1 Ether 125 | >>> bob = create_account(evm, value=int(1e18)) 126 | 127 | # create an account for Alice with no balance 128 | >>> alice = create_account(evm) 129 | 130 | # transfer 1 Ether from Bob to Alice 131 | >>> evm.transfer(bob, alice, int(1e18)) 132 | 133 | # check Alice's balance 134 | >>> evm.get_balance(alice) 135 | 1000000000000000000 136 | 137 | 138 | .. py:method:: advance_block(interval = None) 139 | 140 | This method provides the ability to simulate the mining of blocks. It will advance 141 | `block.number` and `block.timestamp`. 142 | 143 | It's not necessary to call this method. However, some contracts may have logic 144 | that need this information. 145 | 146 | :param interval: (int) optional. set the time in seconds between blocks. Default is 12 seconds 147 | 148 | Example: 149 | 150 | .. code-block:: python 151 | 152 | >>> evm.advance_block() 153 | 154 | 155 | -------------------------------------------------------------------------------- /src/core/fork.rs: -------------------------------------------------------------------------------- 1 | // 2 | 3 | use crate::core::{ 4 | errors::DatabaseError, 5 | fork_backend::ForkBackend, 6 | snapshot::{SnapShot, SnapShotAccountRecord, SnapShotSource}, 7 | }; 8 | use alloy_primitives::U256; 9 | use revm::db::{CacheDB, DatabaseRef}; 10 | use revm::primitives::Address; 11 | use revm::primitives::{Account, AccountInfo, Bytecode, HashMap as Map, B256}; 12 | use revm::{Database, DatabaseCommit}; 13 | 14 | #[derive(Clone, Debug)] 15 | pub struct Fork { 16 | pub db: CacheDB, 17 | pub block_number: u64, 18 | pub timestamp: u64, 19 | } 20 | 21 | impl Fork { 22 | pub fn new(url: &str, starting_block_number: Option) -> Self { 23 | let backend = ForkBackend::new(url, starting_block_number); 24 | let block_number = backend.block_number; 25 | let timestamp = backend.timestamp; 26 | Self { 27 | db: CacheDB::new(backend), 28 | block_number, 29 | timestamp, 30 | } 31 | } 32 | 33 | pub fn database(&self) -> &CacheDB { 34 | &self.db 35 | } 36 | 37 | pub fn database_mut(&mut self) -> &mut CacheDB { 38 | &mut self.db 39 | } 40 | 41 | pub fn create_snapshot(&self, block_num: u64, timestamp: u64) -> anyhow::Result { 42 | let accounts = self 43 | .database() 44 | .accounts 45 | .clone() 46 | .into_iter() 47 | .map( 48 | |(k, v)| -> anyhow::Result<(Address, SnapShotAccountRecord)> { 49 | let code = if let Some(code) = v.info.code { 50 | code 51 | } else { 52 | self.database().code_by_hash_ref(v.info.code_hash)? 53 | } 54 | .to_checked(); 55 | Ok(( 56 | k, 57 | SnapShotAccountRecord { 58 | nonce: v.info.nonce, 59 | balance: v.info.balance, 60 | code: code.original_bytes(), 61 | storage: v.storage.into_iter().collect(), 62 | }, 63 | )) 64 | }, 65 | ) 66 | .collect::>()?; 67 | Ok(SnapShot { 68 | block_num, 69 | timestamp, 70 | source: SnapShotSource::Fork, 71 | accounts, 72 | }) 73 | } 74 | } 75 | 76 | impl Database for Fork { 77 | type Error = DatabaseError; 78 | 79 | fn basic(&mut self, address: Address) -> Result, Self::Error> { 80 | // Note: this will always return Some, since the `SharedBackend` will always load the 81 | // account, this differs from `::basic`, See also 82 | // [MemDb::ensure_loaded](crate::backend::MemDb::ensure_loaded) 83 | Database::basic(&mut self.db, address) 84 | } 85 | 86 | fn code_by_hash(&mut self, code_hash: B256) -> Result { 87 | Database::code_by_hash(&mut self.db, code_hash) 88 | } 89 | 90 | fn storage(&mut self, address: Address, index: U256) -> Result { 91 | Database::storage(&mut self.db, address, index) 92 | } 93 | 94 | fn block_hash(&mut self, number: U256) -> Result { 95 | Database::block_hash(&mut self.db, number) 96 | } 97 | } 98 | 99 | impl DatabaseRef for Fork { 100 | type Error = DatabaseError; 101 | 102 | fn basic_ref(&self, address: Address) -> Result, Self::Error> { 103 | self.db.basic_ref(address) 104 | } 105 | 106 | fn code_by_hash_ref(&self, code_hash: B256) -> Result { 107 | self.db.code_by_hash_ref(code_hash) 108 | } 109 | 110 | fn storage_ref(&self, address: Address, index: U256) -> Result { 111 | DatabaseRef::storage_ref(&self.db, address, index) 112 | } 113 | 114 | fn block_hash_ref(&self, number: U256) -> Result { 115 | self.db.block_hash_ref(number) 116 | } 117 | } 118 | 119 | impl DatabaseCommit for Fork { 120 | fn commit(&mut self, changes: Map) { 121 | self.database_mut().commit(changes) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /simular/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helper functions 3 | """ 4 | 5 | from secrets import token_hex 6 | from eth_utils import to_wei, is_address 7 | import typing 8 | 9 | from . import PyEvm, PyAbi, Contract 10 | 11 | 12 | def ether_to_wei(value: int) -> int: 13 | """Convert ether to wei""" 14 | return to_wei(value, "ether") 15 | 16 | 17 | def generate_random_address() -> str: 18 | """Generate a random hex encoded account/wallet address 19 | 20 | Returns: the address 21 | """ 22 | return "0x" + token_hex(20) 23 | 24 | 25 | def create_account(evm: PyEvm, address: str = None, value: int = 0) -> str: 26 | """ 27 | Create an account 28 | 29 | - evm : PyEvm. the EVM client 30 | - address: str optional. if set it will be used for the account address. 31 | Otherwise a random address will be generated. 32 | - value : int optional. create an initial balance for the account in wei 33 | 34 | Returns: the address 35 | """ 36 | if not isinstance(evm, PyEvm): 37 | raise Exception("'evm' should be an instance of either PyEvmLocal or PyEvmFork") 38 | 39 | if not address: 40 | address = generate_random_address() 41 | evm.create_account(address, value) 42 | return address 43 | 44 | if not is_address(address): 45 | raise Exception("'address' does not appear to be a valid Ethereum address") 46 | 47 | evm.create_account(address, value) 48 | return address 49 | 50 | 51 | def create_many_accounts(evm: PyEvm, num: int, value: int = 0) -> typing.List[str]: 52 | """ 53 | Create many accounts in the EVM 54 | - evm : PyEvm. the EVM client 55 | - num : int the number of accounts to create 56 | - value : int optional. create an initial balance for each account in wei 57 | 58 | Returns a list of addresses 59 | """ 60 | return [create_account(evm, value=value) for _ in range(num)] 61 | 62 | 63 | def contract_from_raw_abi(evm: PyEvm, raw_abi: str) -> Contract: 64 | """ 65 | Create the contract given the full ABI. Full ABI should include 66 | `abi` and `bytecode`. This is usually a single json file from a compiled Solidity contract. 67 | 68 | - `evm` : PyEvm. the EVM client 69 | - `raw_abi` : abi file as un-parsed json 70 | Returns an instance of Contract 71 | """ 72 | if not isinstance(evm, PyEvm): 73 | raise Exception("'evm' should be an instance of either PyEvmLocal or PyEvmFork") 74 | 75 | if not isinstance(raw_abi, str): 76 | raise Exception("expected a an un-parsed json file") 77 | 78 | abi = PyAbi.from_full_json(raw_abi) 79 | return Contract(evm, abi) 80 | 81 | 82 | def contract_from_abi_bytecode( 83 | evm: PyEvm, raw_abi: str, bytecode: bytes = None 84 | ) -> Contract: 85 | """ 86 | Create a contract given the abi and/or bytecode. 87 | 88 | - `evm` : PyEvm the EVM client 89 | - `raw_abi` : abi file as un-parsed json 90 | - `bytecode`: Optional bytes 91 | Returns an instance of Contract 92 | """ 93 | if not isinstance(evm, PyEvm): 94 | raise Exception("'evm' should be an instance of either PyEvmLocal or PyEvmFork") 95 | 96 | if not isinstance(raw_abi, str): 97 | raise Exception("expected a an un-parsed json file") 98 | 99 | abi = PyAbi.from_abi_bytecode(raw_abi, bytecode) 100 | return Contract(evm, abi) 101 | 102 | 103 | def contract_from_inline_abi(evm: PyEvm, abi: typing.List[str]) -> Contract: 104 | """ 105 | Create the contract using inline ABI. 106 | - `evm` : PyEvm. the EVM client 107 | - `abi` : a list of strings that describe the solidity functions of interest. 108 | Returns an instance of Contract 109 | 110 | Function are described in the format: 'function NAME(PARAMETER TYPES) (RETURN TYPES)' 111 | where: 112 | `NAME` if the function name 113 | `PARAMETER TYPES are 0 or more solidity types of any arguments to the function 114 | `RETURN TYPES are any expected returned solidity types. If the function does not return 115 | anything, this is not needed. 116 | 117 | Examples: 118 | - "function hello(uint256,uint256)`: hello function the expects 2 int arguments and returns nothing 119 | - "function hello()(uint256)"`: hello function with no arguments and return an int 120 | 121 | abi = ['function hello()(uint256)', 'function world(string) (string)'] 122 | 123 | """ 124 | if not isinstance(evm, PyEvm): 125 | raise Exception("'evm' should be an instance of either PyEvmLocal or PyEvmFork") 126 | 127 | abi = PyAbi.from_human_readable(abi) 128 | return Contract(evm, abi) 129 | -------------------------------------------------------------------------------- /simular/simular.pyi: -------------------------------------------------------------------------------- 1 | from typing import Optional, Type, List, Tuple, Any, Dict 2 | 3 | class TxResult: 4 | @property 5 | def output(self) -> Optional[Any]: 6 | """ 7 | Return the output of the transaction, if any 8 | """ 9 | 10 | @property 11 | def event(self) -> Optional[Dict[str, Any]]: 12 | """ 13 | Return a dict of events, where: 14 | - key: name of the event 15 | - value: event information 16 | """ 17 | 18 | @property 19 | def gas_used(self) -> int: 20 | """ 21 | Return the amount of gas used by the transaction 22 | """ 23 | 24 | class PyEvm: 25 | def __new__(cls: Type["PyEvm"]) -> "PyEvm": 26 | """ 27 | Create an instance of the Evm using In-memory storage 28 | """ 29 | 30 | @staticmethod 31 | def from_fork( 32 | cls: Type["PyEvm"], url: str, blocknumber: Optional[int] = None 33 | ) -> "PyEvm": 34 | """ 35 | Create an EVM configured to use a remote node to load state data. 36 | 37 | - `url`: the URL of the remote node to connect to 38 | - `blockchain`: optional block to start. Default is 'latest' 39 | """ 40 | 41 | @staticmethod 42 | def from_snapshot(raw: str) -> "PyEvm": 43 | """ 44 | Create an EVM loading state from a snapshot. 45 | 46 | - `raw`: the snapshot data 47 | """ 48 | 49 | def create_snapshot(self) -> str: 50 | """ 51 | Create a snapshot by saving EVM state to str. 52 | """ 53 | 54 | def create_account(self, address: str, balance: Optional[int] = 0): 55 | """ 56 | Create an account. 57 | 58 | - `address`: the address for the account 59 | - `balance`: optional amount of Wei to fund the account. Default = 0 60 | """ 61 | 62 | def get_balance(self, user: str) -> int: 63 | """ 64 | Return the balance of the given user. Where 'user' is the address. 65 | """ 66 | 67 | def transfer(self, caller: str, to: str, amount: int): 68 | """ 69 | Transfer an 'amount' of Wei/Eth from 'caller' -> 'to' 70 | 71 | - `caller`: sender 72 | - `to`: recipient 73 | - `amount`: amount to transfer 74 | """ 75 | 76 | def deploy(self, args: str, caller: str, value: int, abi: PyAbi) -> str: 77 | """ 78 | Deploy a contract. See `Contract` for the recommended way to use this. 79 | """ 80 | 81 | def advance_block(self, interval: Optional[int] = 12): 82 | """ 83 | Advance the block.number / block.timestamp. 84 | 85 | - `interval`: optional. block time interval in seconds. Default: 12 86 | """ 87 | 88 | class PyAbi: 89 | """ 90 | Load, parse, and encode Solidity ABI information 91 | """ 92 | 93 | @staticmethod 94 | def from_full_json(abi: str) -> "PyAbi": 95 | """ 96 | Load from a file that contains both ABI and bytecode information. 97 | For example, the output from compiling a contract with Foundry 98 | 99 | - `abi`: the str version of the compiled output file 100 | """ 101 | 102 | @staticmethod 103 | def from_abi_bytecode(abi: str, data: Optional[bytes]) -> "PyAbi": 104 | """ 105 | Load the ABI and optionally the bytecode 106 | 107 | - `abi`: just the abi information 108 | - `data`: optionally the contract bytecode 109 | """ 110 | 111 | @staticmethod 112 | def from_human_readable(values: List[str]) -> "PyAbi": 113 | """ 114 | Load from a list of contract function definitions. 115 | 116 | - `values`: list of function definitions 117 | 118 | For example: values = [ 119 | 'function hello() returns (bool)', 120 | 'function add(uint256, uint256). 121 | ] 122 | 123 | Would provide the ABI to encode the 'hello' and 'add' functions 124 | """ 125 | 126 | def has_function(self, name: str) -> bool: 127 | """ 128 | Does the contract have the function with the given name? 129 | 130 | - `name`: the function name 131 | """ 132 | 133 | def has_fallback(self) -> bool: 134 | """ 135 | Does the contract have a fallback? 136 | """ 137 | 138 | def has_receive(self) -> bool: 139 | """ 140 | Does the contract have a receive? 141 | """ 142 | 143 | def bytecode(self) -> Optional[bytes]: 144 | """ 145 | Return the bytecode for the contract or None 146 | """ 147 | 148 | def encode_constructor(self, args: str) -> Tuple[bytes, bool]: 149 | """ 150 | ABI encode contract constructor. This is a low-level call. 151 | See `Contract` 152 | """ 153 | 154 | def encode_function( 155 | self, name: str, args: str 156 | ) -> Tuple[bytes, bool, "DynSolTypeWrapper"]: 157 | """ 158 | ABI encode a function. This is a low-level call. 159 | See `Contract` 160 | 161 | - `name`: name of the function 162 | - `args`: arguments to the function 163 | """ 164 | 165 | class DynSolTypeWrapper: 166 | def __new__(cls: Type["DynSolTypeWrapper"]) -> "DynSolTypeWrapper": ... 167 | -------------------------------------------------------------------------------- /tests/fixtures/BlockMeta.json: -------------------------------------------------------------------------------- 1 | { 2 | "abi": [ 3 | { 4 | "type": "function", 5 | "name": "getMeta", 6 | "inputs": [], 7 | "outputs": [ 8 | { 9 | "name": "", 10 | "type": "uint256", 11 | "internalType": "uint256" 12 | }, 13 | { 14 | "name": "", 15 | "type": "uint256", 16 | "internalType": "uint256" 17 | } 18 | ], 19 | "stateMutability": "view" 20 | } 21 | ], 22 | "bytecode": { 23 | "object": "0x6080604052348015600f57600080fd5b50607c80601d6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063a79af2ce14602d575b600080fd5b6040805142815243602082015281519081900390910190f3fea26469706673582212202c76d8081bf4b8745cf50463d5b4f48aadbd688456ec111406e9010a51d456ba64736f6c63430008150033", 24 | "sourceMap": "65:175:22:-:0;;;;;;;;;;;;;;;;;;;", 25 | "linkReferences": {} 26 | }, 27 | "deployedBytecode": { 28 | "object": "0x6080604052348015600f57600080fd5b506004361060285760003560e01c8063a79af2ce14602d575b600080fd5b6040805142815243602082015281519081900390910190f3fea26469706673582212202c76d8081bf4b8745cf50463d5b4f48aadbd688456ec111406e9010a51d456ba64736f6c63430008150033", 29 | "sourceMap": "65:175:22:-:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;129:109;;;;201:15;188:25:35;;218:12:22;244:2:35;229:18;;222:34;129:109:22;;;;;;;;;;", 30 | "linkReferences": {} 31 | }, 32 | "methodIdentifiers": { 33 | "getMeta()": "a79af2ce" 34 | }, 35 | "rawMetadata": "{\"compiler\":{\"version\":\"0.8.21+commit.d9974bed\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[],\"name\":\"getMeta\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"}],\"devdoc\":{\"kind\":\"dev\",\"methods\":{},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{\"getMeta()\":{\"notice\":\"Used for testing the simulator\"}},\"version\":1}},\"settings\":{\"compilationTarget\":{\"src/BlockMeta.sol\":\"BlockMeta\"},\"evmVersion\":\"paris\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\"},\"optimizer\":{\"enabled\":true,\"runs\":200},\"remappings\":[\":ds-test/=lib/forge-std/lib/ds-test/src/\",\":forge-std/=lib/forge-std/src/\",\":solmate/=lib/solmate/src/\"]},\"sources\":{\"src/BlockMeta.sol\":{\"keccak256\":\"0xbdc5d72b6d5eab867385d279eea3a36dc70fc21dd91750790e83f0e8724dc718\",\"license\":\"UNLICENSED\",\"urls\":[\"bzz-raw://c7122fdef2316f20f01744bc285901301dcbbbb3a3441c911ab4fbc4301356a2\",\"dweb:/ipfs/QmR9WJihJPtrXqmn1ifD8mVQxAvyBGr7MBSf4ojKY52adf\"]}},\"version\":1}", 36 | "metadata": { 37 | "compiler": { 38 | "version": "0.8.21+commit.d9974bed" 39 | }, 40 | "language": "Solidity", 41 | "output": { 42 | "abi": [ 43 | { 44 | "inputs": [], 45 | "stateMutability": "view", 46 | "type": "function", 47 | "name": "getMeta", 48 | "outputs": [ 49 | { 50 | "internalType": "uint256", 51 | "name": "", 52 | "type": "uint256" 53 | }, 54 | { 55 | "internalType": "uint256", 56 | "name": "", 57 | "type": "uint256" 58 | } 59 | ] 60 | } 61 | ], 62 | "devdoc": { 63 | "kind": "dev", 64 | "methods": {}, 65 | "version": 1 66 | }, 67 | "userdoc": { 68 | "kind": "user", 69 | "methods": { 70 | "getMeta()": { 71 | "notice": "Used for testing the simulator" 72 | } 73 | }, 74 | "version": 1 75 | } 76 | }, 77 | "settings": { 78 | "remappings": [ 79 | "ds-test/=lib/forge-std/lib/ds-test/src/", 80 | "forge-std/=lib/forge-std/src/", 81 | "solmate/=lib/solmate/src/" 82 | ], 83 | "optimizer": { 84 | "enabled": true, 85 | "runs": 200 86 | }, 87 | "metadata": { 88 | "bytecodeHash": "ipfs" 89 | }, 90 | "compilationTarget": { 91 | "src/BlockMeta.sol": "BlockMeta" 92 | }, 93 | "evmVersion": "paris", 94 | "libraries": {} 95 | }, 96 | "sources": { 97 | "src/BlockMeta.sol": { 98 | "keccak256": "0xbdc5d72b6d5eab867385d279eea3a36dc70fc21dd91750790e83f0e8724dc718", 99 | "urls": [ 100 | "bzz-raw://c7122fdef2316f20f01744bc285901301dcbbbb3a3441c911ab4fbc4301356a2", 101 | "dweb:/ipfs/QmR9WJihJPtrXqmn1ifD8mVQxAvyBGr7MBSf4ojKY52adf" 102 | ], 103 | "license": "UNLICENSED" 104 | } 105 | }, 106 | "version": 1 107 | }, 108 | "id": 22 109 | } -------------------------------------------------------------------------------- /simular/contract.py: -------------------------------------------------------------------------------- 1 | """ 2 | Wraps pyo3 code to provide a high-level contract API 3 | """ 4 | 5 | from eth_utils import is_address 6 | import typing 7 | 8 | from .simular import PyEvm, PyAbi, TxResult 9 | 10 | 11 | def convert_for_soltypes(args: typing.Tuple) -> str: 12 | """ 13 | This converts `args` into a string for processing on the Rust side 14 | """ 15 | if not isinstance(args, tuple): 16 | raise Exception("Expected tuple as arguments") 17 | 18 | def clean(v): 19 | return str(v).replace("'", "").replace("True", "true").replace("False", "false") 20 | 21 | if len(args) == 1: 22 | return f"({clean(args[0])})" 23 | return clean(args) 24 | 25 | 26 | class Function: 27 | """ 28 | Contains information needed to interact with a contract function. This 29 | is attached automatically to the Contract based on the ABI. 30 | """ 31 | 32 | def __init__(self, evm: PyEvm, abi: PyAbi, contract_address: str, name: str): 33 | self.name = name 34 | self.evm = evm 35 | self.abi = abi 36 | self.contract_address = contract_address 37 | 38 | def call(self, *args) -> typing.Any: 39 | """ 40 | Make a read-only call to the contract, returning any results. Solidity 41 | read-only calls are marked as `view` or `pure`. Does not commit any state 42 | changes to the Evm. 43 | 44 | - `args`: 0 or more expected arguments to the function 45 | 46 | Returns: the decoded result 47 | """ 48 | if not self.contract_address: 49 | raise Exception("missing contract address. see at() method") 50 | 51 | stargs = convert_for_soltypes(args) 52 | result = self.evm.call(self.name, stargs, self.contract_address, self.abi) 53 | return result 54 | 55 | def simulate(self, *args, caller: str = None, value: int = 0) -> "TxResult": 56 | """ 57 | Simulate a write call to the contract w/o changing state. 58 | """ 59 | if not self.contract_address: 60 | raise Exception("missing contract address. see at() method") 61 | 62 | if not is_address(caller): 63 | raise Exception("caller is missing or is not a valid address") 64 | 65 | stargs = convert_for_soltypes(args) 66 | result = self.evm.simulate( 67 | self.name, stargs, caller, self.contract_address, value, self.abi 68 | ) 69 | return result 70 | 71 | def transact(self, *args, caller: str = None, value: int = 0) -> "TxResult": 72 | """ 73 | Make a write call to the contract changing the state of the Evm. 74 | - `args`: 0 or more expected arguments to the function 75 | - `caller`: the address of the caller. This translates to `msg.sender` in a Solidity 76 | - `value` : an optional amount of Ether to send with the value ... `msg.value` 77 | Returns: the decoded result 78 | """ 79 | if not self.contract_address: 80 | raise Exception("missing contract address. see at() method") 81 | 82 | if not is_address(caller): 83 | raise Exception("caller is missing or is not a valid address") 84 | 85 | stargs = convert_for_soltypes(args) 86 | result = self.evm.transact( 87 | self.name, stargs, caller, self.contract_address, value, self.abi 88 | ) 89 | return result 90 | 91 | 92 | class Contract: 93 | def __init__(self, evm: PyEvm, abi: PyAbi): 94 | """ 95 | Instantiate a contract from an ABI with an EVM. 96 | 97 | Maps contract functions to this class. Making function available 98 | as attributes on the Contract. 99 | 100 | See `utils.py` for helper functions to create a Contract 101 | """ 102 | self.address = None 103 | self.evm = evm 104 | self.abi = abi 105 | 106 | def __getattr__(self, name: str) -> Function: 107 | """ 108 | Make solidity contract methods available as method calls. For a given function `name`, 109 | return `Function`. 110 | 111 | For example, if the ABI has the contract function 'function hello(uint256)', 112 | you can invoke it by name: contract.hello.transact(10) 113 | """ 114 | if self.abi.has_function(name): 115 | return Function(self.evm, self.abi, self.address, name) 116 | else: 117 | raise Exception(f"contract function: '{name}' not found!") 118 | 119 | def at(self, address: str) -> "Contract": 120 | """ 121 | Set the contract address. 122 | 123 | .. note:: 124 | this is automatically set when using deploy`` 125 | 126 | Parameters 127 | ---------- 128 | address: str 129 | the address of a deployed contract 130 | 131 | Returns 132 | ------- 133 | self 134 | """ 135 | self.address = address 136 | return self 137 | 138 | def deploy(self, *args, caller: str = None, value: int = 0) -> str: 139 | """ 140 | Deploy the contract, returning it's deployed address 141 | - `args`: a list of args (if any) 142 | - `caller`: the address of the requester...`msg.sender` 143 | - `value`: optional amount of Ether for the contract 144 | Returns the address of the deployed contract 145 | """ 146 | if not caller: 147 | raise Exception("Missing required 'caller' address") 148 | 149 | if not is_address(caller): 150 | raise Exception("'caller' is not a valid ethereum address") 151 | 152 | stargs = convert_for_soltypes(args) 153 | addr = self.evm.deploy(stargs, caller, value, self.abi) 154 | self.address = addr 155 | return addr 156 | -------------------------------------------------------------------------------- /src/core/in_memory_db.rs: -------------------------------------------------------------------------------- 1 | //! The in memory DB 2 | //! ADAPTED FROM Foundry-rs 3 | //! https://github.com/foundry-rs/foundry/blob/master/crates/evm/core/src/backend/in_memory_db.rs 4 | //! 5 | use crate::core::{ 6 | errors::DatabaseError, 7 | snapshot::{SnapShot, SnapShotAccountRecord, SnapShotSource}, 8 | }; 9 | use alloy_primitives::{Address, B256, U256}; 10 | use revm::{ 11 | db::{CacheDB, DatabaseRef, EmptyDB}, 12 | primitives::{Account, AccountInfo, Bytecode, HashMap as Map}, 13 | Database, DatabaseCommit, 14 | }; 15 | 16 | /// 17 | /// This acts like a wrapper type for [InMemoryDB] but is capable of creating/applying snapshots 18 | #[derive(Debug)] 19 | pub struct MemDb { 20 | pub db: CacheDB, 21 | } 22 | 23 | impl Default for MemDb { 24 | fn default() -> Self { 25 | Self { 26 | db: CacheDB::new(Default::default()), 27 | } 28 | } 29 | } 30 | 31 | impl MemDb { 32 | pub fn create_snapshot(&self, block_num: u64, timestamp: u64) -> anyhow::Result { 33 | let accounts = self 34 | .db 35 | .accounts 36 | .clone() 37 | .into_iter() 38 | .map( 39 | |(k, v)| -> anyhow::Result<(Address, SnapShotAccountRecord)> { 40 | let code = if let Some(code) = v.info.code { 41 | code 42 | } else { 43 | self.db.code_by_hash_ref(v.info.code_hash)? 44 | } 45 | .to_checked(); 46 | Ok(( 47 | k, 48 | SnapShotAccountRecord { 49 | nonce: v.info.nonce, 50 | balance: v.info.balance, 51 | code: code.original_bytes(), 52 | storage: v.storage.into_iter().collect(), 53 | }, 54 | )) 55 | }, 56 | ) 57 | .collect::>()?; 58 | Ok(SnapShot { 59 | block_num, 60 | timestamp, 61 | source: SnapShotSource::Memory, 62 | accounts, 63 | }) 64 | } 65 | } 66 | 67 | impl DatabaseRef for MemDb { 68 | type Error = DatabaseError; 69 | fn basic_ref(&self, address: Address) -> Result, Self::Error> { 70 | DatabaseRef::basic_ref(&self.db, address) 71 | } 72 | 73 | fn code_by_hash_ref(&self, code_hash: B256) -> Result { 74 | DatabaseRef::code_by_hash_ref(&self.db, code_hash) 75 | } 76 | 77 | fn storage_ref(&self, address: Address, index: U256) -> Result { 78 | DatabaseRef::storage_ref(&self.db, address, index) 79 | } 80 | 81 | fn block_hash_ref(&self, number: U256) -> Result { 82 | DatabaseRef::block_hash_ref(&self.db, number) 83 | } 84 | } 85 | 86 | impl Database for MemDb { 87 | type Error = DatabaseError; 88 | 89 | fn basic(&mut self, address: Address) -> Result, Self::Error> { 90 | // Note: this will always return `Some(AccountInfo)`, See `EmptyDBWrapper` 91 | Database::basic(&mut self.db, address) 92 | } 93 | 94 | fn code_by_hash(&mut self, code_hash: B256) -> Result { 95 | Database::code_by_hash(&mut self.db, code_hash) 96 | } 97 | 98 | fn storage(&mut self, address: Address, index: U256) -> Result { 99 | Database::storage(&mut self.db, address, index) 100 | } 101 | 102 | fn block_hash(&mut self, number: U256) -> Result { 103 | Database::block_hash(&mut self.db, number) 104 | } 105 | } 106 | 107 | impl DatabaseCommit for MemDb { 108 | fn commit(&mut self, changes: Map) { 109 | DatabaseCommit::commit(&mut self.db, changes) 110 | } 111 | } 112 | 113 | /// *documentation from foundry-rs* 114 | /// 115 | /// An empty database that always returns default values when queried. 116 | /// 117 | /// This is just a simple wrapper for `revm::EmptyDB` but implements `DatabaseError` instead, this 118 | /// way we can unify all different `Database` impls 119 | /// 120 | /// This will also _always_ return `Some(AccountInfo)`: 121 | /// 122 | /// The [`Database`](revm::Database) implementation for `CacheDB` manages an `AccountState` for the 123 | /// `DbAccount`, this will be set to `AccountState::NotExisting` if the account does not exist yet. 124 | /// This is because there's a distinction between "non-existing" and "empty", 125 | /// see . 126 | /// If an account is `NotExisting`, `Database::basic_ref` will always return `None` for the 127 | /// requested `AccountInfo`. 128 | /// 129 | /// To prevent this, we ensure that a missing account is never marked as `NotExisting` by always 130 | /// returning `Some` with this type, which will then insert a default [`AccountInfo`] instead 131 | /// of one marked as `AccountState::NotExisting`. 132 | #[derive(Clone, Debug, Default)] 133 | pub struct EmptyDBWrapper(EmptyDB); 134 | 135 | impl DatabaseRef for EmptyDBWrapper { 136 | type Error = DatabaseError; 137 | 138 | fn basic_ref(&self, _address: Address) -> Result, Self::Error> { 139 | // Note: this will always return `Some(AccountInfo)`, for the reason explained above 140 | Ok(Some(AccountInfo::default())) 141 | } 142 | 143 | fn code_by_hash_ref(&self, code_hash: B256) -> Result { 144 | Ok(self.0.code_by_hash_ref(code_hash)?) 145 | } 146 | fn storage_ref(&self, address: Address, index: U256) -> Result { 147 | Ok(self.0.storage_ref(address, index)?) 148 | } 149 | 150 | fn block_hash_ref(&self, number: U256) -> Result { 151 | Ok(self.0.block_hash_ref(number)?) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/core/fork_backend.rs: -------------------------------------------------------------------------------- 1 | use alloy_primitives::{Address, U256}; 2 | use anyhow::Result; 3 | use ethers_core::types::{Block, BlockId, BlockNumber, TxHash, H160, H256, U64}; 4 | use ethers_providers::{Http, Middleware, Provider, ProviderError}; 5 | use revm::{ 6 | primitives::{AccountInfo, Bytecode, B256, KECCAK_EMPTY}, 7 | DatabaseRef, 8 | }; 9 | use std::sync::Arc; 10 | use tokio::runtime::{Builder, Handle, RuntimeFlavor}; 11 | 12 | use crate::core::errors::DatabaseError; 13 | 14 | pub type HttpProvider = Provider; 15 | 16 | #[derive(Clone, Debug)] 17 | pub struct ForkBackend { 18 | provider: Arc, 19 | pub block_number: u64, 20 | pub timestamp: u64, 21 | } 22 | 23 | impl ForkBackend { 24 | pub fn new(url: &str, starting_block_number: Option) -> Self { 25 | let client = 26 | Provider::::try_from(url).expect("ForkBackend: failed to load HTTP provider"); 27 | let provider = Arc::new(client); 28 | 29 | let blockid = if let Some(bn) = starting_block_number { 30 | BlockId::from(U64::from(bn)) 31 | } else { 32 | BlockId::from(BlockNumber::Latest) 33 | }; 34 | 35 | let blk = match Self::block_on(provider.get_block(blockid)) { 36 | Ok(Some(b)) => b, 37 | _ => panic!("ForkBackend: failed to load block information"), 38 | }; 39 | 40 | let block_number = blk 41 | .number 42 | .expect("ForkBackend: Got 'pending' block number") 43 | .as_u64(); 44 | let timestamp = blk.timestamp.as_u64(); 45 | /* 46 | let block_number = if let Some(bn) = starting_block_number { 47 | bn 48 | } else { 49 | Self::block_on(provider.get_block_number()) 50 | .expect("ForkBackend: failed to load latest blocknumber from remote") 51 | .as_u64() 52 | }; 53 | */ 54 | 55 | Self { 56 | provider, 57 | block_number, 58 | timestamp, 59 | } 60 | } 61 | 62 | // adapted from revm ethersdb 63 | #[inline] 64 | fn block_on(f: F) -> F::Output 65 | where 66 | F: core::future::Future + Send, 67 | F::Output: Send, 68 | { 69 | match Handle::try_current() { 70 | Ok(handle) => match handle.runtime_flavor() { 71 | RuntimeFlavor::CurrentThread => std::thread::scope(move |s| { 72 | s.spawn(move || { 73 | Builder::new_current_thread() 74 | .enable_all() 75 | .build() 76 | .unwrap() 77 | .block_on(f) 78 | }) 79 | .join() 80 | .unwrap() 81 | }), 82 | _ => tokio::task::block_in_place(move || handle.block_on(f)), 83 | }, 84 | Err(_) => Builder::new_current_thread() 85 | .enable_all() 86 | .build() 87 | .unwrap() 88 | .block_on(f), 89 | } 90 | } 91 | 92 | fn fetch_basic_from_fork(&self, address: Address) -> Result { 93 | let add = H160::from(address.0 .0); 94 | let bn: Option = Some(BlockId::from(self.block_number)); 95 | 96 | let f = async { 97 | let nonce = self.provider.get_transaction_count(add, bn); 98 | let balance = self.provider.get_balance(add, bn); 99 | let code = self.provider.get_code(add, bn); 100 | tokio::join!(nonce, balance, code) 101 | }; 102 | let (nonce, balance, code) = Self::block_on(f); 103 | 104 | let balance = U256::from_limbs(balance?.0); 105 | let nonce = nonce?.as_u64(); 106 | let bytecode = Bytecode::new_raw(code?.0.into()); 107 | let code_hash = bytecode.hash_slow(); 108 | Ok(AccountInfo::new(balance, nonce, code_hash, bytecode)) 109 | } 110 | 111 | fn fetch_storage_from_fork( 112 | &self, 113 | address: Address, 114 | index: U256, 115 | ) -> Result { 116 | let add = H160::from(address.0 .0); 117 | let bn: Option = Some(BlockId::from(self.block_number)); 118 | 119 | let index = H256::from(index.to_be_bytes()); 120 | let slot_value: H256 = Self::block_on(self.provider.get_storage_at(add, index, bn))?; 121 | Ok(U256::from_be_bytes(slot_value.to_fixed_bytes())) 122 | } 123 | 124 | fn fetch_blockhash_from_fork(&self, number: U256) -> Result { 125 | if number > U256::from(u64::MAX) { 126 | return Ok(KECCAK_EMPTY); 127 | } 128 | // We know number <= u64::MAX so unwrap is safe 129 | let number = U64::from(u64::try_from(number).unwrap()); 130 | let block: Option> = 131 | Self::block_on(self.provider.get_block(BlockId::from(number)))?; 132 | Ok(B256::new(block.unwrap().hash.unwrap().0)) 133 | } 134 | } 135 | 136 | impl DatabaseRef for ForkBackend { 137 | type Error = DatabaseError; 138 | 139 | fn basic_ref(&self, address: Address) -> Result, Self::Error> { 140 | match self.fetch_basic_from_fork(address) { 141 | Ok(addr) => Ok(Some(addr)), 142 | Err(_err) => Err(DatabaseError::GetAccount(address)), 143 | } 144 | } 145 | 146 | fn code_by_hash_ref(&self, hash: B256) -> Result { 147 | Err(DatabaseError::MissingCode(hash)) 148 | } 149 | 150 | fn storage_ref(&self, address: Address, index: U256) -> Result { 151 | self.fetch_storage_from_fork(address, index) 152 | .map_err(|_err| DatabaseError::GetStorage(address, index)) 153 | } 154 | 155 | fn block_hash_ref(&self, number: U256) -> Result { 156 | self.fetch_blockhash_from_fork(number) 157 | .map_err(|_err| DatabaseError::GetBlockHash(number)) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /docs/contract.rst: -------------------------------------------------------------------------------- 1 | .. _contract: 2 | 3 | Contract 4 | ======== 5 | 6 | .. contents:: :local: 7 | 8 | 9 | .. py:module:: simular.contract 10 | 11 | 12 | Provides a wrapper around the core Rust libraries implementing the EVM and ABI parser. 13 | Solidity contract methods are extracted from the ABI and made available as attributes 14 | on the instance of a ``Contract``. 15 | 16 | For example, given the following Solidity contract: 17 | 18 | .. code-block:: javascript 19 | 20 | contract HelloWorld { 21 | address public owner; 22 | uint256 public value; 23 | 24 | constructor() { 25 | owner = msg.sender 26 | } 27 | 28 | // add the number and returns the new value 29 | function addNum(uint256 num) public returns (uint256) { 30 | value += num; 31 | return value; 32 | } 33 | } 34 | 35 | ``Contract`` will parse the ABI and make all the functions available as attributes 36 | using Python's ``__getattr__``. To execute the function you can use one of the following: 37 | 38 | .. code-block:: python 39 | 40 | # Send a write transaction to the contract. This will change state in the EVM. 41 | tx_result = transact(*args, caller: str = None, value: int = 0) 42 | 43 | # Send a read transaction to the contract. This will NOT change state 44 | result = call(*args) 45 | 46 | # Like transact but it will NOT change state. 47 | tx_result = simulate(*args, caller: str = None, value: int = 0) 48 | 49 | Note that both ``transact`` and ``simulate`` return a ``TxResult`` object that contains 50 | any return values from the call, and any events emitted. See ``TxResult`` below. 51 | 52 | 53 | Example... 54 | 55 | .. code-block:: python 56 | 57 | >>> contract = Contract(evm, abi) 58 | 59 | # Return the owner address from the contract 60 | >>> owner_address = contract.owner.call() 61 | 62 | # Add 3 to the contract's 'value' 63 | >>> tx_result = contract.addNum.transact(3, caller=bob) 64 | 65 | # you can access any return values via the ``output`` attribute 66 | >>> print(tx_result.output) 67 | 3 68 | 69 | Format: 70 | 71 | ``contract.attributename.[call(...), transact(...), simulate(...)]`` 72 | 73 | Properties 74 | ---------- 75 | 76 | .. py:attribute:: evm 77 | 78 | The instance of the embedded EVM 79 | 80 | .. py:attribute:: abi 81 | 82 | An instance of the ABI parser 83 | 84 | .. py:attribute:: address 85 | 86 | The address of the contract. This is available for a deployed contract 87 | 88 | 89 | Constructor 90 | ----------- 91 | 92 | .. py:class:: Contract(evm: PyEvm, abi: PyAbi) 93 | 94 | Represents a contract and all the functions defined in the Solidity Contract. All calls 95 | are translated into Ethereum transactions and sent to the EVM. 96 | 97 | .. note:: 98 | 99 | The preferred way to create a contract is to use one of the ``contract_*`` 100 | functions in :ref:`utils` 101 | 102 | :param evm: an instance of the EVM 103 | :type evm: PyEvm 104 | :param abi: an instance of the Abi parser 105 | :type abi: PyAbi 106 | :return: an instance of the contract 107 | 108 | .. code-block:: python 109 | 110 | >>> evm = PyEvm() 111 | >>> contract = Contract(evm) 112 | 113 | 114 | Methods 115 | -------- 116 | 117 | .. py:method:: at(address: str) 118 | 119 | Set the address for the contract. This is automatically done when using ``deploy`` 120 | 121 | :param address: the address of the deployed contract 122 | 123 | 124 | .. py:method:: deploy(*args, caller: str = None, value: int = 0) -> str 125 | 126 | Deploy a contract to the EVM. Under the covers, it uses the ABI to encode 127 | the constructor call to make a transaction. 128 | 129 | :param args: 0 or more arguments expected by the Contract's constructor 130 | :param caller: the address making the deploy. this is `msg.sender` 131 | :param value: (optional) amount of `wei` to send to the contract. This will fail if the contracts constructor is not mark as ``payable`` 132 | :return: the address of the deployed contract 133 | :raises Exception: If ``caller`` is not provided OR ``caller`` is not a valid address 134 | 135 | Example: 136 | 137 | Assume the ``HelloWorld.json`` is the compiled Solidy ABI 138 | 139 | .. code-block:: python 140 | 141 | # imports 142 | >>> from simular import PyEvm, contract_from_raw_abi, create_account 143 | 144 | # load the json file 145 | >>> with open('HelloWorld.json') as f: 146 | ... abi = f.read() 147 | 148 | # create an instance of the EVM 149 | >>> evm = PyEvm() 150 | 151 | # create an account to deploy the contract 152 | >>> bob = create_account() 153 | 154 | # create an instance of the contract from the abi 155 | >>> contract = contract_from_raw_abi(abi) 156 | 157 | # deploy the contract, returning it's address 158 | >>> contract.deploy(caller=bob) 159 | '0x0091410228bf6062ab28c949ba4172ee9144bfde' 160 | 161 | .. py:method:: transact(*args, caller: str = None, value: int = 0) -> TxResult 162 | 163 | Execute a write transaction to the contract. This will change the state of the contract 164 | 165 | .. note:: 166 | Remember this is method is appended to the end of the Solidity contract's function name: 167 | ``contract.[attribute name].transact(...)`` 168 | 169 | :param args: 0 or more arguments expected by the Contract's function 170 | :param caller: (required) the address making the call. this is `msg.sender` 171 | :param value: (optional) amount of `wei` to send to the contract. This will fail if the contracts function is not mark as ``payable`` 172 | :return: the TxResult 173 | :raises Exception: If ``caller`` is not provided OR ``caller`` is not a valid address 174 | 175 | .. py:method:: call(*args) -> Any 176 | 177 | Execute a read transaction to the contract. This will NOT change the state of the contract 178 | 179 | .. note:: 180 | Remember this is method is appended to the end of the Solidity contract's function name: 181 | ``contract.[attribute name].call(...)`` 182 | 183 | :param args: 0 or more arguments expected by the Contract's function 184 | :return: the result of the function call (if any) 185 | :raises Exception: If the contract does not have an address 186 | 187 | 188 | .. py:method:: simulate(*args, caller: str = None, value: int = 0) -> TxResult 189 | 190 | Just like ``transact``. Except it will NOT change the state of the contract. Can be 191 | used to test a ``transact``. 192 | 193 | .. note:: 194 | Remember this is method is appended to the end of the Solidity contract's function name: 195 | ``contract.[attribute name].simulate(...)`` 196 | 197 | :param args: 0 or more arguments expected by the Contract's function 198 | :param caller: (required) the address making the call. this is `msg.sender` 199 | :param value: (optional) amount of `wei` to send to the contract. This will fail if the contracts function is not mark as ``payable`` 200 | :return: the TxResult 201 | :raises Exception: If ``caller`` is not provided OR ``caller`` is not a valid address 202 | 203 | 204 | .. py:class:: TxResult 205 | 206 | Container holding the results of a ``transact`` or ``simulate`` 207 | 208 | .. py:attribute:: output: 209 | 210 | The return value from the contract. May be None 211 | 212 | .. py:attribute:: event 213 | 214 | Map of any emitted events, where each parsed event is keyed by the event name. May be None 215 | 216 | .. py:attribute:: gas_used 217 | 218 | Amount of gas used for the transaction 219 | 220 | 221 | 222 | -------------------------------------------------------------------------------- /tests/fixtures/erc20.bin: -------------------------------------------------------------------------------- 1 | 60e06040523480156200001157600080fd5b5060405162001104380380620011048339810160408190526200003491620001ed565b828282600062000045848262000301565b50600162000054838262000301565b5060ff81166080524660a0526200006a6200008c565b60c0525050600680546001600160a01b03191633179055506200044b92505050565b60007f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f6000604051620000c09190620003cd565b6040805191829003822060208301939093528101919091527fc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc660608201524660808201523060a082015260c00160405160208183030381529060405280519060200120905090565b634e487b7160e01b600052604160045260246000fd5b600082601f8301126200015057600080fd5b81516001600160401b03808211156200016d576200016d62000128565b604051601f8301601f19908116603f0116810190828211818310171562000198576200019862000128565b81604052838152602092508683858801011115620001b557600080fd5b600091505b83821015620001d95785820183015181830184015290820190620001ba565b600093810190920192909252949350505050565b6000806000606084860312156200020357600080fd5b83516001600160401b03808211156200021b57600080fd5b62000229878388016200013e565b945060208601519150808211156200024057600080fd5b506200024f868287016200013e565b925050604084015160ff811681146200026757600080fd5b809150509250925092565b600181811c908216806200028757607f821691505b602082108103620002a857634e487b7160e01b600052602260045260246000fd5b50919050565b601f821115620002fc57600081815260208120601f850160051c81016020861015620002d75750805b601f850160051c820191505b81811015620002f857828155600101620002e3565b5050505b505050565b81516001600160401b038111156200031d576200031d62000128565b62000335816200032e845462000272565b84620002ae565b602080601f8311600181146200036d5760008415620003545750858301515b600019600386901b1c1916600185901b178555620002f8565b600085815260208120601f198616915b828110156200039e578886015182559484019460019091019084016200037d565b5085821015620003bd5787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b6000808354620003dd8162000272565b60018281168015620003f857600181146200040e576200043f565b60ff19841687528215158302870194506200043f565b8760005260208060002060005b85811015620004365781548a8201529084019082016200041b565b50505082870194505b50929695505050505050565b60805160a05160c051610c896200047b60003960006104a6015260006104710152600061016a0152610c896000f3fe608060405234801561001057600080fd5b50600436106100f55760003560e01c806370a08231116100975780639dc29fac116100665780639dc29fac1461022e578063a9059cbb14610241578063d505accf14610254578063dd62ed3e1461026757600080fd5b806370a08231146101bb5780637ecebe00146101db5780638da5cb5b146101fb57806395d89b411461022657600080fd5b806323b872dd116100d357806323b872dd14610152578063313ce567146101655780633644e5151461019e57806340c10f19146101a657600080fd5b806306fdde03146100fa578063095ea7b31461011857806318160ddd1461013b575b600080fd5b610102610292565b60405161010f9190610986565b60405180910390f35b61012b6101263660046109f0565b610320565b604051901515815260200161010f565b61014460025481565b60405190815260200161010f565b61012b610160366004610a1a565b61038d565b61018c7f000000000000000000000000000000000000000000000000000000000000000081565b60405160ff909116815260200161010f565b61014461046d565b6101b96101b43660046109f0565b6104c8565b005b6101446101c9366004610a56565b60036020526000908152604090205481565b6101446101e9366004610a56565b60056020526000908152604090205481565b60065461020e906001600160a01b031681565b6040516001600160a01b03909116815260200161010f565b610102610525565b6101b961023c3660046109f0565b610532565b61012b61024f3660046109f0565b610586565b6101b9610262366004610a78565b6105ec565b610144610275366004610aeb565b600460209081526000928352604080842090915290825290205481565b6000805461029f90610b1e565b80601f01602080910402602001604051908101604052809291908181526020018280546102cb90610b1e565b80156103185780601f106102ed57610100808354040283529160200191610318565b820191906000526020600020905b8154815290600101906020018083116102fb57829003601f168201915b505050505081565b3360008181526004602090815260408083206001600160a01b038716808552925280832085905551919290917f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9259061037b9086815260200190565b60405180910390a35060015b92915050565b6001600160a01b038316600090815260046020908152604080832033845290915281205460001981146103e9576103c48382610b6e565b6001600160a01b03861660009081526004602090815260408083203384529091529020555b6001600160a01b03851660009081526003602052604081208054859290610411908490610b6e565b90915550506001600160a01b0380851660008181526003602052604090819020805487019055519091871690600080516020610c348339815191529061045a9087815260200190565b60405180910390a3506001949350505050565b60007f000000000000000000000000000000000000000000000000000000000000000046146104a35761049e610830565b905090565b507f000000000000000000000000000000000000000000000000000000000000000090565b6006546001600160a01b031633146105175760405162461bcd60e51b815260206004820152600d60248201526c3737ba103a34329037bbb732b960991b60448201526064015b60405180910390fd5b61052182826108ca565b5050565b6001805461029f90610b1e565b6006546001600160a01b0316331461057c5760405162461bcd60e51b815260206004820152600d60248201526c3737ba103a34329037bbb732b960991b604482015260640161050e565b6105218282610924565b336000908152600360205260408120805483919083906105a7908490610b6e565b90915550506001600160a01b03831660008181526003602052604090819020805485019055513390600080516020610c348339815191529061037b9086815260200190565b4284101561063c5760405162461bcd60e51b815260206004820152601760248201527f5045524d49545f444541444c494e455f45585049524544000000000000000000604482015260640161050e565b6000600161064861046d565b6001600160a01b038a811660008181526005602090815260409182902080546001810190915582517f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c98184015280840194909452938d166060840152608083018c905260a083019390935260c08083018b90528151808403909101815260e08301909152805192019190912061190160f01b6101008301526101028201929092526101228101919091526101420160408051601f198184030181528282528051602091820120600084529083018083525260ff871690820152606081018590526080810184905260a0016020604051602081039080840390855afa158015610754573d6000803e3d6000fd5b5050604051601f1901519150506001600160a01b0381161580159061078a5750876001600160a01b0316816001600160a01b0316145b6107c75760405162461bcd60e51b815260206004820152600e60248201526d24a72b20a624a22fa9a4a3a722a960911b604482015260640161050e565b6001600160a01b0390811660009081526004602090815260408083208a8516808552908352928190208990555188815291928a16917f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925910160405180910390a350505050505050565b60007f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f60006040516108629190610b81565b6040805191829003822060208301939093528101919091527fc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc660608201524660808201523060a082015260c00160405160208183030381529060405280519060200120905090565b80600260008282546108dc9190610c20565b90915550506001600160a01b038216600081815260036020908152604080832080548601905551848152600080516020610c3483398151915291015b60405180910390a35050565b6001600160a01b0382166000908152600360205260408120805483929061094c908490610b6e565b90915550506002805482900390556040518181526000906001600160a01b03841690600080516020610c3483398151915290602001610918565b600060208083528351808285015260005b818110156109b357858101830151858201604001528201610997565b506000604082860101526040601f19601f8301168501019250505092915050565b80356001600160a01b03811681146109eb57600080fd5b919050565b60008060408385031215610a0357600080fd5b610a0c836109d4565b946020939093013593505050565b600080600060608486031215610a2f57600080fd5b610a38846109d4565b9250610a46602085016109d4565b9150604084013590509250925092565b600060208284031215610a6857600080fd5b610a71826109d4565b9392505050565b600080600080600080600060e0888a031215610a9357600080fd5b610a9c886109d4565b9650610aaa602089016109d4565b95506040880135945060608801359350608088013560ff81168114610ace57600080fd5b9699959850939692959460a0840135945060c09093013592915050565b60008060408385031215610afe57600080fd5b610b07836109d4565b9150610b15602084016109d4565b90509250929050565b600181811c90821680610b3257607f821691505b602082108103610b5257634e487b7160e01b600052602260045260246000fd5b50919050565b634e487b7160e01b600052601160045260246000fd5b8181038181111561038757610387610b58565b600080835481600182811c915080831680610b9d57607f831692505b60208084108203610bbc57634e487b7160e01b86526022600452602486fd5b818015610bd05760018114610be557610c12565b60ff1986168952841515850289019650610c12565b60008a81526020902060005b86811015610c0a5781548b820152908501908301610bf1565b505084890196505b509498975050505050505050565b8082018082111561038757610387610b5856feddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa264697066735822122051e9befe3bd87171d1c485c9b767aa84f4466af5e5fbd2a7e389f62bbfc968b664736f6c63430008140033 2 | 3 | -------------------------------------------------------------------------------- /src/core/storage.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Provides access to EVM storage 3 | //! 4 | 5 | use alloy_primitives::{Address, U256}; 6 | use anyhow::{anyhow, Result}; 7 | use revm::{ 8 | interpreter::primitives::EnvWithHandlerCfg, 9 | primitives::{ 10 | Account, AccountInfo, Bytecode, HashMap as Map, ResultAndState, B256, KECCAK_EMPTY, 11 | }, 12 | Database, DatabaseCommit, DatabaseRef, EvmBuilder, 13 | }; 14 | use std::time::{SystemTime, UNIX_EPOCH}; 15 | 16 | use crate::core::{errors::DatabaseError, snapshot::SnapShot}; 17 | use crate::core::{fork::Fork, in_memory_db::MemDb}; 18 | 19 | /// Information related to creating a fork 20 | #[derive(Clone, Debug)] 21 | pub struct CreateFork { 22 | /// the url of the RPC endpoint 23 | pub url: String, 24 | /// optional block number of the fork. If none, it will use the latest block. 25 | pub blocknumber: Option, 26 | } 27 | 28 | /* 29 | impl CreateFork { 30 | /// Fork at the given URL and block number 31 | pub fn new(url: String, blocknumber: Option) -> Self { 32 | Self { url, blocknumber } 33 | } 34 | 35 | /// For at the given URL and use the latest block available 36 | pub fn latest_block(url: String) -> Self { 37 | Self { 38 | url, 39 | blocknumber: None, 40 | } 41 | } 42 | } 43 | */ 44 | 45 | // Used by the EVM to access storage. This can either be an in-memory only db or a forked db. 46 | // The EVM delegates transact() and transact_commit to this module 47 | // 48 | // This is based heavily on Foundry's approach. 49 | pub struct StorageBackend { 50 | mem_db: MemDb, // impl wrapper to handle DbErrors 51 | forkdb: Option, 52 | pub block_number: u64, // used to record in the snapshot... 53 | pub timestamp: u64, 54 | } 55 | 56 | impl Default for StorageBackend { 57 | fn default() -> Self { 58 | StorageBackend::new(None) 59 | } 60 | } 61 | 62 | impl StorageBackend { 63 | pub fn new(fork: Option) -> Self { 64 | if let Some(fork) = fork { 65 | let backend = Fork::new(&fork.url, fork.blocknumber); 66 | let block_number = backend.block_number; 67 | let timestamp = backend.timestamp; 68 | Self { 69 | mem_db: MemDb::default(), 70 | forkdb: Some(backend), 71 | block_number, 72 | timestamp, 73 | } 74 | } else { 75 | let timestamp = SystemTime::now() 76 | .duration_since(UNIX_EPOCH) 77 | .expect("StorageBackend: failed to get unix epoch time") 78 | .as_secs(); 79 | Self { 80 | mem_db: MemDb::default(), 81 | forkdb: None, 82 | block_number: 1, 83 | timestamp, 84 | } 85 | } 86 | } 87 | 88 | pub fn insert_account_info(&mut self, address: Address, info: AccountInfo) { 89 | if let Some(fork) = self.forkdb.as_mut() { 90 | fork.database_mut().insert_account_info(address, info) 91 | } else { 92 | // use mem... 93 | self.mem_db.db.insert_account_info(address, info) 94 | } 95 | } 96 | 97 | /* 98 | pub fn insert_account_storage( 99 | &mut self, 100 | address: Address, 101 | slot: U256, 102 | value: U256, 103 | ) -> Result<(), DatabaseError> { 104 | let ret = if let Some(fork) = self.forkdb.as_mut() { 105 | fork.database_mut() 106 | .insert_account_storage(address, slot, value) 107 | } else { 108 | self.mem_db.db.insert_account_storage(address, slot, value) 109 | }; 110 | ret 111 | } 112 | 113 | pub fn replace_account_storage( 114 | &mut self, 115 | address: Address, 116 | storage: Map, 117 | ) -> Result<(), DatabaseError> { 118 | if let Some(fork) = self.forkdb.as_mut() { 119 | fork.database_mut() 120 | .replace_account_storage(address, storage) 121 | } else { 122 | self.mem_db.db.replace_account_storage(address, storage) 123 | } 124 | } 125 | */ 126 | 127 | pub fn run_transact(&mut self, env: &mut EnvWithHandlerCfg) -> Result { 128 | let mut evm = create_evm(self, env.clone()); 129 | let res = evm 130 | .transact() 131 | .map_err(|e| anyhow!("backend failed while executing transaction: {:?}", e))?; 132 | env.env = evm.context.evm.inner.env; 133 | 134 | Ok(res) 135 | } 136 | 137 | /// Create a snapshot of the current state, delegates 138 | /// to the current backend database. 139 | pub fn create_snapshot(&self) -> Result { 140 | if let Some(fork) = self.forkdb.as_ref() { 141 | fork.create_snapshot(self.block_number, self.timestamp) 142 | } else { 143 | self.mem_db 144 | .create_snapshot(self.block_number, self.timestamp) 145 | } 146 | } 147 | 148 | /// Load a snapshot into an in-memory database 149 | pub fn load_snapshot(&mut self, snapshot: SnapShot) { 150 | self.block_number = snapshot.block_num; 151 | self.timestamp = snapshot.timestamp; 152 | 153 | for (addr, account) in snapshot.accounts.into_iter() { 154 | // note: this will populate both 'accounts' and 'contracts' 155 | self.mem_db.db.insert_account_info( 156 | addr, 157 | AccountInfo { 158 | balance: account.balance, 159 | nonce: account.nonce, 160 | code_hash: KECCAK_EMPTY, 161 | code: if account.code.0.is_empty() { 162 | None 163 | } else { 164 | Some( 165 | Bytecode::new_raw(alloy_primitives::Bytes(account.code.0)).to_checked(), 166 | ) 167 | }, 168 | }, 169 | ); 170 | 171 | // ... but we still need to load the account storage map 172 | for (k, v) in account.storage.into_iter() { 173 | self.mem_db 174 | .db 175 | .accounts 176 | .entry(addr) 177 | .or_default() 178 | .storage 179 | .insert(k, v); 180 | } 181 | } 182 | } 183 | 184 | /// See EVM update_block 185 | pub fn update_block_info(&mut self, interval: u64) { 186 | self.block_number += 1; 187 | self.timestamp += interval; 188 | } 189 | } 190 | 191 | impl DatabaseRef for StorageBackend { 192 | type Error = DatabaseError; 193 | 194 | fn basic_ref(&self, address: Address) -> Result, Self::Error> { 195 | if let Some(db) = self.forkdb.as_ref() { 196 | db.basic_ref(address) 197 | } else { 198 | Ok(self.mem_db.basic_ref(address)?) 199 | } 200 | } 201 | 202 | fn code_by_hash_ref(&self, code_hash: B256) -> Result { 203 | if let Some(db) = self.forkdb.as_ref() { 204 | db.code_by_hash_ref(code_hash) 205 | } else { 206 | Ok(self.mem_db.code_by_hash_ref(code_hash)?) 207 | } 208 | } 209 | 210 | fn storage_ref(&self, address: Address, index: U256) -> Result { 211 | if let Some(db) = self.forkdb.as_ref() { 212 | DatabaseRef::storage_ref(db, address, index) 213 | } else { 214 | Ok(DatabaseRef::storage_ref(&self.mem_db, address, index)?) 215 | } 216 | } 217 | 218 | fn block_hash_ref(&self, number: U256) -> Result { 219 | if let Some(db) = self.forkdb.as_ref() { 220 | db.block_hash_ref(number) 221 | } else { 222 | Ok(self.mem_db.block_hash_ref(number)?) 223 | } 224 | } 225 | } 226 | 227 | impl Database for StorageBackend { 228 | type Error = DatabaseError; 229 | fn basic(&mut self, address: Address) -> Result, Self::Error> { 230 | if let Some(db) = self.forkdb.as_mut() { 231 | db.basic(address) 232 | } else { 233 | Ok(self.mem_db.basic(address)?) 234 | } 235 | } 236 | 237 | fn code_by_hash(&mut self, code_hash: B256) -> Result { 238 | if let Some(db) = self.forkdb.as_mut() { 239 | db.code_by_hash(code_hash) 240 | } else { 241 | Ok(self.mem_db.code_by_hash(code_hash)?) 242 | } 243 | } 244 | 245 | fn storage(&mut self, address: Address, index: U256) -> Result { 246 | if let Some(db) = self.forkdb.as_mut() { 247 | Database::storage(db, address, index) 248 | } else { 249 | Ok(Database::storage(&mut self.mem_db, address, index)?) 250 | } 251 | } 252 | 253 | fn block_hash(&mut self, number: U256) -> Result { 254 | if let Some(db) = self.forkdb.as_mut() { 255 | db.block_hash(number) 256 | } else { 257 | Ok(self.mem_db.block_hash(number)?) 258 | } 259 | } 260 | } 261 | 262 | impl DatabaseCommit for StorageBackend { 263 | fn commit(&mut self, changes: Map) { 264 | if let Some(db) = self.forkdb.as_mut() { 265 | db.commit(changes) 266 | } else { 267 | self.mem_db.commit(changes) 268 | } 269 | } 270 | } 271 | 272 | fn create_evm<'a, DB: Database>( 273 | db: DB, 274 | env: revm::primitives::EnvWithHandlerCfg, 275 | ) -> revm::Evm<'a, (), DB> { 276 | EvmBuilder::default() 277 | .with_db(db) 278 | .with_env(env.env.clone()) 279 | .build() 280 | } 281 | -------------------------------------------------------------------------------- /src/pyevm.rs: -------------------------------------------------------------------------------- 1 | use crate::core::{evm::BaseEvm, evm::CallResult, snapshot::SnapShot, storage::CreateFork}; 2 | use alloy_dyn_abi::DynSolValue; 3 | use alloy_primitives::U256; 4 | use anyhow::{anyhow, Result}; 5 | use core::ffi::c_uchar; 6 | use pyo3::{ffi, prelude::*, IntoPyObjectExt}; 7 | use std::collections::HashMap; 8 | 9 | use crate::{ 10 | pyabi::{DynSolTypeWrapper, PyAbi}, 11 | str_to_address, 12 | }; 13 | 14 | /// default block interval for advancing block time (12s) 15 | const DEFAULT_BLOCK_INTERVAL: u64 = 12; 16 | 17 | /// Container to hold the results of calling `transact` or `simulate` 18 | #[derive(Debug)] 19 | #[pyclass] 20 | pub struct TxResult { 21 | /// contract function call return value, if any 22 | #[pyo3(get)] 23 | pub output: Option, 24 | /// emitted event information, if any 25 | #[pyo3(get)] 26 | pub event: Option>, 27 | #[pyo3(get)] 28 | pub gas_used: u64, 29 | } 30 | 31 | #[pyclass] 32 | pub struct PyEvm(BaseEvm); 33 | 34 | #[pymethods] 35 | impl PyEvm { 36 | /// Create an in-memory EVM 37 | #[new] 38 | pub fn new() -> Self { 39 | Self(BaseEvm::default()) 40 | } 41 | 42 | /// Create a fork EVM 43 | #[staticmethod] 44 | #[pyo3(signature = (url, blocknumber=None))] 45 | pub fn from_fork(url: &str, blocknumber: Option) -> Self { 46 | let forkinfo = CreateFork { 47 | url: url.into(), 48 | blocknumber, 49 | }; 50 | Self(BaseEvm::new(Some(forkinfo))) 51 | } 52 | 53 | /// Create an in-memory EVM from a `SnapShot` 54 | #[staticmethod] 55 | pub fn from_snapshot(raw: &str) -> Self { 56 | let snap: SnapShot = serde_json::from_str(raw).expect("unable to parse raw snapshot"); 57 | Self(BaseEvm::new_from_snapshot(snap)) 58 | } 59 | 60 | /// Create a `SnapShot` of the current EVM state 61 | pub fn create_snapshot(&self) -> Result { 62 | let snapshot = self.0.create_snapshot()?; 63 | serde_json::to_string_pretty(&snapshot).map_err(|e| anyhow!("{:?}", e)) 64 | } 65 | 66 | /// Create account with an initial balance 67 | #[pyo3(signature = (address, balance=None))] 68 | pub fn create_account(&mut self, address: &str, balance: Option) -> Result<()> { 69 | let caller = str_to_address(address)?; 70 | let value = balance.map(U256::from); 71 | self.0.create_account(caller, value) 72 | } 73 | 74 | /// Get the balance of the given user 75 | pub fn get_balance(&mut self, user: &str) -> Result { 76 | let user = str_to_address(user)?; 77 | let v = self.0.get_balance(user)?; 78 | Ok(v.to::()) 79 | } 80 | 81 | /// Transfer the amount of value from `caller` to the given recipient `to`. 82 | pub fn transfer(&mut self, caller: &str, to: &str, amount: u128) -> Result<()> { 83 | let a = str_to_address(caller)?; 84 | let b = str_to_address(to)?; 85 | let value = U256::try_from(amount)?; 86 | self.0.transfer(a, b, value) 87 | } 88 | 89 | /// Deploy a contract 90 | pub fn deploy(&mut self, args: &str, caller: &str, value: u128, abi: &PyAbi) -> Result { 91 | let a = str_to_address(caller)?; 92 | let v = U256::try_from(value)?; 93 | let (bits, _is_payable) = abi.encode_constructor(args)?; 94 | let addy = self.0.deploy(a, bits, v)?; 95 | Ok(addy.to_string()) 96 | } 97 | 98 | /// Transaction (write) operation to a contract at the given address `to`. This 99 | /// will change state in the EVM. 100 | /// 101 | /// Returns any results of the call and a map of any emitted events. 102 | /// Where the event map is: 103 | /// `key` is the name of the event 104 | /// `value` is the decoded log 105 | pub fn transact( 106 | &mut self, 107 | fn_name: &str, 108 | args: &str, 109 | caller: &str, 110 | to: &str, 111 | value: u128, 112 | abi: &PyAbi, 113 | py: Python<'_>, 114 | ) -> Result { 115 | let a = str_to_address(caller)?; 116 | let b = str_to_address(to)?; 117 | let v = U256::try_from(value)?; 118 | let (calldata, _is_payable, decoder) = abi.encode_function(fn_name, args)?; 119 | let output = self.0.transact_commit(a, b, calldata, v)?; 120 | process_results_and_events(abi, output, decoder, py) 121 | } 122 | 123 | /// Transaction (read) operation to a contract at the given address `to`. This 124 | /// will NOT change state in the EVM. 125 | /// 126 | /// Returns any results of the call 127 | pub fn call( 128 | &mut self, 129 | fn_name: &str, 130 | args: &str, 131 | to: &str, 132 | abi: &PyAbi, 133 | py: Python<'_>, 134 | ) -> Result> { 135 | let to_address = str_to_address(to)?; 136 | let (calldata, _is_payable, decoder) = abi.encode_function(fn_name, args)?; 137 | let output = self.0.transact_call(to_address, calldata, U256::from(0))?; 138 | let res = process_results(output, decoder, py); 139 | Ok(res) 140 | } 141 | 142 | /// Transaction operation to a contract at the given address `to`. This 143 | /// can simulate a transact operation, but will NOT change state in the EVM. 144 | /// 145 | /// Returns any results of the call and a map of any emitted events. 146 | /// Where the event map is: 147 | /// `key` is the name of the event 148 | /// `value` is the decoded log 149 | pub fn simulate( 150 | &mut self, 151 | fn_name: &str, 152 | args: &str, 153 | caller: &str, 154 | to: &str, 155 | value: u128, 156 | abi: &PyAbi, 157 | py: Python<'_>, 158 | ) -> Result { 159 | let caller_address = str_to_address(caller)?; 160 | let to_address = str_to_address(to)?; 161 | let v = U256::try_from(value)?; 162 | let (calldata, _is_payable, decoder) = abi.encode_function(fn_name, args)?; 163 | let output = self.0.simulate(caller_address, to_address, calldata, v)?; 164 | process_results_and_events(abi, output, decoder, py) 165 | } 166 | 167 | /// Advance block.number and block.timestamp. Set interval to the amount of 168 | /// time in seconds you want to advance the timestamp (default: 12s). Block 169 | /// number will automatically increment. 170 | /// 171 | /// When using a fork the initial block.number/timestamp will come from the snapshot. 172 | #[pyo3(signature = (interval=None))] 173 | pub fn advance_block(&mut self, interval: Option) { 174 | let it = interval.unwrap_or(DEFAULT_BLOCK_INTERVAL); 175 | self.0.update_block(it); 176 | } 177 | } 178 | 179 | // *** lil' Helpers *** // 180 | 181 | fn process_results( 182 | output: CallResult, 183 | decoder: DynSolTypeWrapper, 184 | py: Python<'_>, 185 | ) -> Option { 186 | if let Some(de) = decoder.0 { 187 | let dynvalues = de.abi_decode(&output.result).unwrap(); 188 | let d = DynSolMap(dynvalues.clone()); 189 | Some(d.into_py_any(py).unwrap()) 190 | } else { 191 | None 192 | } 193 | } 194 | 195 | // convert results and events to Python 196 | fn process_results_and_events( 197 | abi: &PyAbi, 198 | output_result: CallResult, 199 | decoder: DynSolTypeWrapper, 200 | py: Python<'_>, 201 | ) -> Result { 202 | let logs = output_result.logs.clone(); 203 | let gas_used = output_result.gas_used.clone(); 204 | 205 | // process return value 206 | let output = process_results(output_result, decoder, py); 207 | 208 | // process logs 209 | let event = if logs.len() > 0 { 210 | let raw_events = abi.0.extract_logs(logs); 211 | let mut map = HashMap::::new(); 212 | for (k, v) in raw_events { 213 | let d = DynSolMap(v); 214 | map.insert(k, d.into_py_any(py).unwrap()); 215 | } 216 | Some(map) 217 | } else { 218 | None 219 | }; 220 | Ok(TxResult { 221 | output, 222 | event, 223 | gas_used, 224 | }) 225 | } 226 | 227 | fn walk_list(values: Vec, py: Python<'_>) -> PyObject { 228 | values 229 | .into_iter() 230 | .map(|dv| base_exctract(dv, py)) 231 | .collect::>() 232 | .into_py_any(py) 233 | .unwrap() 234 | } 235 | 236 | // Convert DynSolValue signed and unsigned ints to Python. 237 | // these values originate from Solidity types. 238 | // Goal is to support u/i8 -> u/i256 239 | fn convert_ints(bytes: [u8; 32], is_signed: bool, py: Python<'_>) -> PyObject { 240 | let signed = if is_signed { 1 } else { 0 }; 241 | unsafe { 242 | let obj = 243 | ffi::_PyLong_FromByteArray(bytes.as_ptr().cast::(), bytes.len(), 1, signed); 244 | PyObject::from_owned_ptr(py, obj) 245 | } 246 | } 247 | 248 | // Transform DynSolValues to Python types. 249 | fn base_exctract(dv: DynSolValue, py: Python<'_>) -> PyObject { 250 | match dv { 251 | DynSolValue::Address(a) => format!("{a:?}").into_pyobject(py).unwrap().into(), 252 | DynSolValue::Bool(a) => a.into_py_any(py).unwrap(), 253 | DynSolValue::String(a) => a.into_pyobject(py).unwrap().into(), 254 | DynSolValue::Tuple(a) => walk_list(a, py), 255 | DynSolValue::Int(a, _) => convert_ints(a.to_le_bytes::<32>(), true, py), 256 | DynSolValue::Uint(a, _) => convert_ints(a.to_le_bytes::<32>(), false, py), 257 | DynSolValue::Bytes(a) => a.to_vec().into_py_any(py).unwrap(), 258 | DynSolValue::FixedBytes(a, _) => a.to_vec().into_py_any(py).unwrap(), 259 | DynSolValue::Array(a) => walk_list(a, py), 260 | DynSolValue::FixedArray(a) => walk_list(a, py), 261 | _ => unimplemented!(), 262 | } 263 | } 264 | 265 | pub struct DynSolMap(DynSolValue); 266 | 267 | impl<'py> IntoPyObject<'py> for DynSolMap { 268 | type Target = PyAny; // the Python type 269 | type Output = Bound<'py, Self::Target>; 270 | type Error = std::convert::Infallible; 271 | 272 | fn into_pyobject(self, py: Python<'py>) -> Result { 273 | Ok(base_exctract(self.0, py).into_bound(py)) 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright (c) 2023 The MITRE Corporation 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /src/core/abi.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Parse contract ABIs to encode, decode contract calls 3 | //! 4 | use alloy_dyn_abi::{DynSolEvent, DynSolType, DynSolValue, Specifier}; 5 | use alloy_json_abi::{ContractObject, Function, JsonAbi, StateMutability}; 6 | use alloy_primitives::{Bytes, Log, LogData}; 7 | use anyhow::{anyhow, bail, Result}; 8 | use std::collections::BTreeMap; 9 | 10 | type EventMap = BTreeMap>; 11 | 12 | /// 13 | /// Wrapper around pre-processed Events to help extract log information. 14 | /// We flatten the structure of `events` in JsonAbi to make it easier to 15 | /// automatically decode Logs from a `transact/simulate`. 16 | /// 17 | /// EventLog contains `DynSolEvent` to be used to decode log information 18 | #[derive(Debug)] 19 | pub struct EventLog { 20 | /// the event name 21 | pub name: String, 22 | /// the decoder resolved from the original event 23 | pub decoder: DynSolEvent, 24 | } 25 | 26 | impl EventLog { 27 | /// Attempt to decode the log and return the event name and extracted values as 28 | /// DynSolValues. 29 | pub fn decode(&self, log: &LogData) -> Option<(String, DynSolValue)> { 30 | if let Ok(r) = self.decoder.decode_log_data(log, true) { 31 | let v = DynSolValue::Tuple([r.indexed, r.body].concat()); 32 | return Some((self.name.clone(), v)); 33 | } 34 | None 35 | } 36 | } 37 | 38 | pub struct ContractAbi { 39 | /// alloy's json abi object 40 | pub abi: JsonAbi, 41 | /// optional contract bytecode 42 | pub bytecode: Option, 43 | /// Contract event information with a log decoder 44 | pub events_logs: Vec, 45 | } 46 | 47 | // walk through the events in JsonAbi to flatten the 48 | // structure and convert to `EventLog`. 49 | fn convert_events(ev: &EventMap) -> Vec { 50 | ev.iter() 51 | .flat_map(|(k, v)| { 52 | v.iter() 53 | .map(|e| EventLog { 54 | name: k.clone(), 55 | decoder: e.resolve().unwrap(), 56 | }) 57 | .collect::>() 58 | }) 59 | .collect::>() 60 | } 61 | 62 | impl ContractAbi { 63 | /// Parse the `abi` and `bytecode` from a compiled contract's json file. 64 | /// Note: `raw` is un-parsed json. 65 | pub fn from_full_json(raw: &str) -> Self { 66 | let co = 67 | serde_json::from_str::(raw).expect("Abi: failed to parse abi to json"); 68 | if co.abi.is_none() { 69 | panic!("Abi: ABI not found in file") 70 | } 71 | if co.bytecode.is_none() { 72 | panic!("Abi: Bytecode not found in file") 73 | } 74 | let abi = co.abi.unwrap(); 75 | let evts = convert_events(&abi.events); 76 | Self { 77 | abi, 78 | bytecode: co.bytecode, 79 | events_logs: evts, 80 | } 81 | } 82 | 83 | /// Parse the `abi` and `bytecode` 84 | /// Note: `raw` is un-parsed json. 85 | pub fn from_abi_bytecode(raw: &str, bytecode: Option>) -> Self { 86 | let abi = serde_json::from_str::(raw).expect("Abi: failed to parse abi"); 87 | let evts = convert_events(&abi.events); 88 | Self { 89 | abi, 90 | bytecode: bytecode.map(Bytes::from), 91 | events_logs: evts, 92 | } 93 | } 94 | 95 | /// Parse an ABI (without bytecode) from a `Vec` of contract function definitions. 96 | /// See [human readable abi](https://docs.ethers.org/v5/api/utils/abi/formats/#abi-formats--human-readable-abi) 97 | pub fn from_human_readable(input: Vec<&str>) -> Self { 98 | let abi = JsonAbi::parse(input).expect("Abi: Invalid solidity function(s) format"); 99 | let evts = convert_events(&abi.events); 100 | Self { 101 | abi, 102 | bytecode: None, 103 | events_logs: evts, 104 | } 105 | } 106 | 107 | /// Extract and decode logs from emitted events 108 | pub fn extract_logs(&self, logs: Vec) -> Vec<(String, DynSolValue)> { 109 | let mut results: Vec<(String, DynSolValue)> = Vec::new(); 110 | for log in logs { 111 | for e in &self.events_logs { 112 | if let Some(p) = e.decode(&log.data) { 113 | results.push(p); 114 | } 115 | } 116 | } 117 | results 118 | } 119 | 120 | /// Is there a function with the given name? 121 | pub fn has_function(&self, name: &str) -> bool { 122 | self.abi.functions.contains_key(name) 123 | } 124 | 125 | /// Does the ABI have a fallback? 126 | pub fn has_fallback(&self) -> bool { 127 | self.abi.fallback.is_some() 128 | } 129 | 130 | /// Does the ABI have a receive? 131 | pub fn has_receive(&self) -> bool { 132 | self.abi.receive.is_some() 133 | } 134 | 135 | /// Return the contract bytecode as a Vec 136 | pub fn bytecode(&self) -> Option> { 137 | self.bytecode.as_ref().map(|b| b.to_vec()) 138 | } 139 | 140 | /// Encode the information needed to create a contract. This will 141 | /// concatenate the contract bytecode with any arguments required by 142 | /// the constructor. Note: `args` is a string of input arguments. See 143 | /// `encode_function` for more information. 144 | pub fn encode_constructor(&self, args: &str) -> Result<(Vec, bool)> { 145 | let bytecode = match self.bytecode() { 146 | Some(b) => b, 147 | _ => bail!("Abi: Missing contract bytecode!"), 148 | }; 149 | 150 | let constructor = match &self.abi.constructor { 151 | Some(c) => c, 152 | _ => return Ok((bytecode, false)), 153 | }; 154 | 155 | let types = constructor 156 | .inputs 157 | .iter() 158 | .map(|i| i.resolve().unwrap()) 159 | .collect::>(); 160 | 161 | let ty = DynSolType::Tuple(types); 162 | let dynavalues = ty.coerce_str(args).map_err(|_| { 163 | anyhow!("Abi: Error coercing the arguments for the constructor. Check the input argument(s)") 164 | })?; 165 | let encoded_args = dynavalues.abi_encode_params(); 166 | let is_payable = matches!(constructor.state_mutability, StateMutability::Payable); 167 | 168 | Ok(([bytecode, encoded_args].concat(), is_payable)) 169 | } 170 | 171 | fn extract(funcs: &Function, args: &str) -> Result { 172 | let types = funcs 173 | .inputs 174 | .iter() 175 | .map(|i| i.resolve().unwrap()) 176 | .collect::>(); 177 | let ty = DynSolType::Tuple(types); 178 | ty.coerce_str(args).map_err(|_| { 179 | anyhow!( 180 | "Abi: Error coercing the arguments for the function call. Check the input argument(s)" 181 | ) 182 | }) 183 | } 184 | 185 | /// Encode function information for use in a transaction. Note: `args` is a string 186 | /// of input parameters that are parsed by alloy `DynSolType`'s and converted into 187 | /// `DynSolValue`s. See [DynSolType.coerce_str()](https://docs.rs/alloy-dyn-abi/latest/alloy_dyn_abi/enum.DynSolType.html#method.coerce_str) 188 | /// 189 | /// - `name` is the name of the function 190 | /// - `args` string of input arguments 191 | /// 192 | /// ## Example 193 | /// 194 | /// `"(1, hello, (0x11111111111111111111111111111, 5))"` 195 | /// 196 | /// is parsed into an alloy `DynSolValue` ...tuple, U256, etc... 197 | /// 198 | /// Returns a tuple with: 199 | /// - encoded function and args 200 | /// - whether the function is payable 201 | /// - and the output `DynSolType` that can be used to decode the result of the call 202 | pub fn encode_function( 203 | &self, 204 | name: &str, 205 | args: &str, 206 | ) -> anyhow::Result<(Vec, bool, Option)> { 207 | let funcs = match self.abi.function(name) { 208 | Some(funcs) => funcs, 209 | _ => bail!("Abi: Function {} not found in the ABI!", name), 210 | }; 211 | 212 | // find the first function that matches the input args 213 | for f in funcs { 214 | let result = Self::extract(f, args); 215 | let is_payable = matches!(f.state_mutability, StateMutability::Payable); 216 | if result.is_ok() { 217 | // Get the return type decoder, if any... 218 | let ty = match f.outputs.len() { 219 | 0 => None, 220 | 1 => f.outputs.first().unwrap().clone().resolve().ok(), 221 | _ => { 222 | let t = f 223 | .outputs 224 | .iter() 225 | .map(|i| i.resolve().unwrap()) 226 | .collect::>(); 227 | Some(DynSolType::Tuple(t)) 228 | } 229 | }; 230 | 231 | let selector = f.selector().to_vec(); 232 | let encoded_args = result.unwrap().abi_encode_params(); 233 | let all = [selector, encoded_args].concat(); 234 | 235 | return Ok((all, is_payable, ty)); 236 | } 237 | } 238 | 239 | // if we get here, it means we didn't find a function that 240 | // matched the input arguments 241 | Err(anyhow::anyhow!( 242 | "Abi: Arguments to the function do not match what is expected" 243 | )) 244 | } 245 | } 246 | 247 | #[cfg(test)] 248 | mod tests { 249 | 250 | use super::*; 251 | use alloy_primitives::{b256, bytes, Address, LogData}; 252 | 253 | #[test] 254 | fn check_constructor_encoding() { 255 | let input = vec!["constructor()"]; 256 | let mut abi = ContractAbi::from_human_readable(input); 257 | // short-circuit internal check... 258 | abi.bytecode = Some(b"hello".into()); 259 | 260 | assert!(abi.encode_constructor("()").is_ok()); 261 | assert!(abi.encode_constructor("(1234)").is_err()); 262 | } 263 | 264 | #[test] 265 | fn encoding_function_decoder_types() { 266 | let tc = ContractAbi::from_human_readable(vec![ 267 | "function a()", 268 | "function b() (uint256)", 269 | "function c() (bool, address, uint256)", 270 | ]); 271 | 272 | let (_, _, r1) = tc.encode_function("a", "()").unwrap(); 273 | let (_, _, r2) = tc.encode_function("b", "()").unwrap(); 274 | let (_, _, r3) = tc.encode_function("c", "()").unwrap(); 275 | 276 | assert_eq!(None, r1); 277 | assert_eq!(Some(DynSolType::Uint(256)), r2); 278 | assert_eq!( 279 | Some(DynSolType::Tuple(vec![ 280 | DynSolType::Bool, 281 | DynSolType::Address, 282 | DynSolType::Uint(256) 283 | ])), 284 | r3 285 | ); 286 | } 287 | 288 | #[test] 289 | fn encoding_functions() { 290 | let hello_world = vec!["function hello(tuple(uint256, address, uint160)) (bool)"]; 291 | let hw = ContractAbi::from_human_readable(hello_world); 292 | assert!(hw.has_function("hello")); 293 | 294 | let addy = Address::with_last_byte(24); 295 | 296 | assert!(hw.encode_function("bob", "()").is_err()); 297 | assert!(hw.encode_function("hello", "(1,2").is_err()); 298 | 299 | let (_, is_payable, dtype) = hw 300 | .encode_function("hello", &format!("(({}, {}, {}))", 10, addy.to_string(), 1)) 301 | .unwrap(); 302 | 303 | assert!(!is_payable); 304 | assert_eq!(dtype, Some(DynSolType::Bool)); 305 | } 306 | 307 | #[test] 308 | fn encoding_overloaded_functions() { 309 | let overit = vec![ 310 | "function one() (bool)", 311 | "function one(uint256)", 312 | "function one(address, (uint64, uint64)) (address)", 313 | ]; 314 | let abi = ContractAbi::from_human_readable(overit); 315 | let addy = Address::with_last_byte(24); 316 | 317 | let (_, _, otype) = abi 318 | .encode_function("one", &format!("({},({},{}))", addy.to_string(), 10, 11)) 319 | .unwrap(); 320 | 321 | assert_eq!(Some(DynSolType::Address), otype); 322 | } 323 | 324 | #[test] 325 | fn test_flatten_event_structure() { 326 | // mint signature: 0x0f6798a560793a54c3bcfe86a93cde1e73087d944c0ea20544137d4121396885 327 | // burn signature: 0xcc16f5dbb4873280815c1ee09dbd06736cffcc184412cf7a71a0fdb75d397ca5 328 | let sample = ContractAbi::from_human_readable(vec![ 329 | "event Transfer(address indexed from,address indexed to,uint256 amount)", 330 | "event Transfer(address indexed from) anonymous", 331 | "event Mint(address indexed recip,uint256 amount)", 332 | "event Burn(address indexed recip,uint256 amount)", 333 | ]); 334 | 335 | assert_eq!(4, sample.events_logs.len()); 336 | 337 | let transfer = LogData::new_unchecked( 338 | vec![ 339 | b256!("ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), 340 | b256!("000000000000000000000000c2e9f25be6257c210d7adf0d4cd6e3e881ba25f8"), 341 | b256!("0000000000000000000000002b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b"), 342 | ], 343 | bytes!("0000000000000000000000000000000000000000000000000000000000000005"), 344 | ); 345 | 346 | let burn = LogData::new_unchecked( 347 | vec![ 348 | b256!("cc16f5dbb4873280815c1ee09dbd06736cffcc184412cf7a71a0fdb75d397ca5"), 349 | b256!("000000000000000000000000c2e9f25be6257c210d7adf0d4cd6e3e881ba25f8"), 350 | ], 351 | bytes!("0000000000000000000000000000000000000000000000000000000000000005"), 352 | ); 353 | 354 | let log_address = Address::repeat_byte(14); 355 | let logs = vec![ 356 | Log { 357 | address: log_address, 358 | data: transfer, 359 | }, 360 | Log { 361 | address: log_address, 362 | data: burn, 363 | }, 364 | ]; 365 | 366 | let results = sample.extract_logs(logs); 367 | assert_eq!(2, results.len()); 368 | 369 | //println!("{:?}", results); 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /src/core/evm.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! An API to interact with an embedded Ethereum Virtual Machine. 3 | //! 4 | //! This is wrapper around [REVM](https://docs.rs/revm/latest/revm/index.html). The implementation 5 | //! is a simplfied version of [Foundry's Executor](https://github.com/foundry-rs/foundry) 6 | //! 7 | 8 | use alloy_primitives::{Address, Bytes, U256}; 9 | use alloy_sol_types::decode_revert_reason; 10 | use anyhow::{anyhow, bail, Result}; 11 | use revm::{ 12 | db::{DatabaseCommit, DatabaseRef}, 13 | primitives::{ 14 | Account, AccountInfo, BlockEnv, Env, EnvWithHandlerCfg, ExecutionResult, HashMap as Map, 15 | Log, Output, ResultAndState, TransactTo, TxEnv, 16 | }, 17 | }; 18 | 19 | use crate::{core::snapshot::SnapShot, core::storage::CreateFork, core::storage::StorageBackend}; 20 | 21 | /// type alias for a `revm` hashmap of `Address` => `Account` 22 | type StateChangeSet = Map; 23 | 24 | /// EVM that supports both in-memory and forked storage. 25 | pub struct BaseEvm { 26 | backend: StorageBackend, 27 | env: EnvWithHandlerCfg, 28 | } 29 | 30 | /// Create an EVM with the in-memory database 31 | impl Default for BaseEvm { 32 | fn default() -> Self { 33 | BaseEvm::new(None) 34 | } 35 | } 36 | 37 | impl BaseEvm { 38 | /// Create an instance of the EVM. If fork is None it will use the in-memory database. 39 | /// Otherwise it will create a forked database. 40 | pub fn new(fork: Option) -> Self { 41 | let env = EnvWithHandlerCfg::default(); 42 | let backend = StorageBackend::new(fork); 43 | Self { env, backend } 44 | } 45 | 46 | /// Create an instance of the EVM and load it's state from the `SnapShot`. This 47 | /// will use the in-memory database. 48 | pub fn new_from_snapshot(snap: SnapShot) -> Self { 49 | let env = EnvWithHandlerCfg::default(); 50 | let mut backend = StorageBackend::default(); 51 | backend.load_snapshot(snap); 52 | Self { env, backend } 53 | } 54 | 55 | /// Create an account for the given `user` with an optional balance (`amount`). 56 | /// This will overwrite an account if it already exists. 57 | pub fn create_account(&mut self, user: Address, amount: Option) -> Result<()> { 58 | let mut info = AccountInfo::default(); 59 | if let Some(amnt) = amount { 60 | info.balance = amnt; 61 | } 62 | self.backend.insert_account_info(user, info); 63 | Ok(()) 64 | } 65 | 66 | /// Return the balance for the `caller`'s account. 67 | pub fn get_balance(&mut self, caller: Address) -> Result { 68 | Ok(self 69 | .backend 70 | .basic_ref(caller)? 71 | .map(|acc| acc.balance) 72 | .unwrap_or_default()) 73 | } 74 | 75 | /* 76 | /// Set the balance for the given `address` with the given `amount` 77 | pub fn set_balance(&mut self, address: Address, amount: U256) -> Result<&mut Self> { 78 | let mut account = self.backend.basic_ref(address)?.unwrap_or_default(); 79 | account.balance = amount; 80 | 81 | self.backend.insert_account_info(address, account); 82 | Ok(self) 83 | } 84 | */ 85 | 86 | /// Create a snapshot of the current database. This can be used to reload state. 87 | pub fn create_snapshot(&self) -> Result { 88 | self.backend.create_snapshot() 89 | } 90 | 91 | /// Deploy a contract returning the contract's address. 92 | /// If `value` is specified, the constructor must be `payable`. 93 | pub fn deploy(&mut self, caller: Address, data: Vec, value: U256) -> Result
{ 94 | let mut env = self.build_env(Some(caller), TransactTo::create(), data.into(), value); 95 | let result = self.backend.run_transact(&mut env)?; 96 | let mut call_results = process_call_result(result)?; 97 | self.commit(&mut call_results); 98 | 99 | match call_results.address { 100 | Some(addr) => Ok(addr), 101 | _ => Err(anyhow!("deploy did not return an Address!")), 102 | } 103 | } 104 | 105 | /// Transfer `value` from `caller` -> `to` 106 | pub fn transfer(&mut self, caller: Address, to: Address, value: U256) -> Result<()> { 107 | let _ = self.transact_commit(caller, to, vec![], value)?; 108 | Ok(()) 109 | } 110 | 111 | /* TODO Remove? 112 | /// Same as `transact_commit`, but supports [alloy's sol types](https://docs.rs/alloy-sol-types/latest/alloy_sol_types/index.html). 113 | pub fn transact_commit_sol( 114 | &mut self, 115 | caller: Address, 116 | to: Address, 117 | args: T, 118 | value: U256, 119 | ) -> Result<::Return> { 120 | let data = args.abi_encode(); 121 | let result = self.transact_commit(caller, to, data, value)?; 122 | T::abi_decode_returns(&result.result, true) 123 | .map_err(|e| anyhow!("transact commit sol error: {:?}", e)) 124 | } 125 | */ 126 | 127 | /// Write call to a contact. Send a transaction where any state changes are persisted to the underlying database. 128 | pub fn transact_commit( 129 | &mut self, 130 | caller: Address, 131 | to: Address, 132 | data: Vec, 133 | value: U256, 134 | ) -> Result { 135 | let mut env = self.build_env(Some(caller), TransactTo::call(to), data.into(), value); 136 | let result = self.backend.run_transact(&mut env)?; 137 | let mut call_results = process_call_result(result)?; 138 | self.commit(&mut call_results); 139 | 140 | Ok(call_results) 141 | } 142 | 143 | /* TODO remove 144 | /// Same as `transact_call` but supports [alloy's sol types](https://docs.rs/alloy-sol-types/latest/alloy_sol_types/index.html). 145 | pub fn transact_call_sol( 146 | &mut self, 147 | to: Address, 148 | args: T, 149 | value: U256, 150 | ) -> Result<::Return> { 151 | let data = args.abi_encode(); 152 | let result = self.transact_call(to, data, value)?; 153 | T::abi_decode_returns(&result.result, true) 154 | .map_err(|e| anyhow!("transact call sol error: {:?}", e)) 155 | } 156 | */ 157 | 158 | /// Read call to a contract. Send a transaction but any state changes are NOT persisted to the 159 | /// database. 160 | pub fn transact_call(&mut self, to: Address, data: Vec, value: U256) -> Result { 161 | let mut env = self.build_env(None, TransactTo::call(to), data.into(), value); 162 | let result = self.backend.run_transact(&mut env)?; 163 | process_call_result(result) 164 | } 165 | 166 | /// Simulate a `transact_commit` without actually committing/changing state. 167 | pub fn simulate( 168 | &mut self, 169 | caller: Address, 170 | to: Address, 171 | data: Vec, 172 | value: U256, 173 | ) -> Result { 174 | let mut env = self.build_env(Some(caller), TransactTo::call(to), data.into(), value); 175 | let result = self.backend.run_transact(&mut env)?; 176 | process_call_result(result) 177 | } 178 | 179 | /// Advance `block.number` and `block.timestamp`. Set `interval` to the 180 | /// amount of time in seconds you want to advance the timestamp. Block number 181 | /// will be automatically incremented. 182 | /// 183 | /// Must be manually called. 184 | pub fn update_block(&mut self, interval: u64) { 185 | self.backend.update_block_info(interval); 186 | } 187 | 188 | fn build_env( 189 | &self, 190 | caller: Option
, 191 | transact_to: TransactTo, 192 | data: Bytes, 193 | value: U256, 194 | ) -> EnvWithHandlerCfg { 195 | let blkn = self.backend.block_number; 196 | let ts = self.backend.timestamp; 197 | 198 | let env = Env { 199 | cfg: self.env.cfg.clone(), 200 | block: BlockEnv { 201 | basefee: U256::ZERO, 202 | timestamp: U256::from(ts), 203 | number: U256::from(blkn), 204 | ..self.env.block.clone() 205 | }, 206 | tx: TxEnv { 207 | caller: caller.unwrap_or(Address::ZERO), 208 | transact_to, 209 | data, 210 | value, 211 | gas_price: U256::ZERO, 212 | gas_priority_fee: None, 213 | ..self.env.tx.clone() 214 | }, 215 | }; 216 | 217 | EnvWithHandlerCfg::new_with_spec_id(Box::new(env), self.env.handler_cfg.spec_id) 218 | } 219 | 220 | fn commit(&mut self, result: &mut CallResult) { 221 | if let Some(changes) = &result.state_changeset { 222 | self.backend.commit(changes.clone()); 223 | } 224 | } 225 | } 226 | 227 | /// Container for the results of a transaction 228 | pub struct CallResult { 229 | /// The raw result of the call. 230 | pub result: Bytes, 231 | /// An address if the call is a TransactTo::create (deploy) 232 | pub address: Option
, 233 | /// The gas used for the call 234 | pub gas_used: u64, 235 | /// Refunded gas 236 | pub gas_refunded: u64, 237 | /// The logs emitted during the call 238 | pub logs: Vec, 239 | /// Changes made to the database 240 | pub state_changeset: Option, 241 | } 242 | 243 | fn process_call_result(result: ResultAndState) -> Result { 244 | let ResultAndState { 245 | result: exec_result, 246 | state: state_changeset, 247 | } = result; 248 | 249 | let (gas_refunded, gas_used, out, logs) = match exec_result { 250 | ExecutionResult::Success { 251 | gas_used, 252 | gas_refunded, 253 | output, 254 | logs, 255 | .. 256 | } => (gas_refunded, gas_used, output, logs), 257 | ExecutionResult::Revert { gas_used, output } => match decode_revert_reason(&output) { 258 | Some(reason) => bail!("Reverted: {:?}. Gas used: {:?}", reason, gas_used), 259 | _ => bail!("Reverted with no reason. Gas used: {:?}", gas_used), 260 | }, 261 | ExecutionResult::Halt { reason, gas_used } => { 262 | bail!("Halted: {:?}. Gas used: {:?}", reason, gas_used) 263 | } 264 | }; 265 | 266 | match out { 267 | Output::Call(result) => Ok(CallResult { 268 | result, 269 | gas_used, 270 | gas_refunded, 271 | logs, 272 | address: None, 273 | state_changeset: Some(state_changeset), 274 | }), 275 | Output::Create(data, address) => Ok(CallResult { 276 | result: data.clone(), 277 | address, 278 | gas_used, 279 | logs, 280 | gas_refunded, 281 | state_changeset: Some(state_changeset), 282 | }), 283 | } 284 | } 285 | 286 | #[cfg(test)] 287 | mod tests { 288 | use crate::core::abi::ContractAbi; 289 | use crate::core::evm::BaseEvm; 290 | use alloy_dyn_abi::DynSolValue; 291 | use alloy_primitives::{Address, U256}; 292 | 293 | const BYTECODE: &str = "608060405260405161032c38038061032c8339810160408190526100\ 294 | 229161003c565b600155600080546001600160a01b03191633179055610055565b6000602\ 295 | 0828403121561004e57600080fd5b5051919050565b6102c8806100646000396000f3fe60\ 296 | 80604052600436106100555760003560e01c80633fa4f2451461005a57806361fa423b146\ 297 | 100835780637cf5dab0146100b35780638da5cb5b146100e8578063d09de08a1461012057\ 298 | 8063d0e30db014610135575b600080fd5b34801561006657600080fd5b506100706001548\ 299 | 1565b6040519081526020015b60405180910390f35b34801561008f57600080fd5b506100\ 300 | a361009e36600461020a565b610137565b604051901515815260200161007a565b3480156\ 301 | 100bf57600080fd5b506100d36100ce366004610222565b6101c8565b6040805192835260\ 302 | 208301919091520161007a565b3480156100f457600080fd5b50600054610108906001600\ 303 | 160a01b031681565b6040516001600160a01b03909116815260200161007a565b34801561\ 304 | 012c57600080fd5b506100706101ec565b005b600080546001600160a01b0316331461018\ 305 | e5760405162461bcd60e51b81526020600482015260156024820152743737ba103a343290\ 306 | 31bab93932b73a1037bbb732b960591b604482015260640160405180910390fd5b61019b6\ 307 | 02083018361023b565b600080546001600160a01b0319166001600160a01b039290921691\ 308 | 90911790555060200135600190815590565b60008082600160008282546101dd919061026\ 309 | b565b90915550506001549293915050565b6001805460009180836101ff828561026b565b\ 310 | 909155509092915050565b60006040828403121561021c57600080fd5b50919050565b600\ 311 | 06020828403121561023457600080fd5b5035919050565b60006020828403121561024d57\ 312 | 600080fd5b81356001600160a01b038116811461026457600080fd5b9392505050565b808\ 313 | 2018082111561028c57634e487b7160e01b600052601160045260246000fd5b9291505056\ 314 | fea264697066735822122073a633ec59ee8e261bbdfefdc6d54f1d47dd6ccd6dcab4aa1eb\ 315 | 37b62d24b4c1b64736f6c63430008140033"; 316 | 317 | #[test] 318 | fn balances() { 319 | let zero = U256::from(0); 320 | //let one_eth = U256::from(1e18); 321 | 322 | let mut evm = BaseEvm::default(); 323 | let bob = Address::repeat_byte(23); 324 | 325 | evm.create_account(bob, None).unwrap(); 326 | assert!(evm.get_balance(bob).unwrap() == zero); 327 | 328 | //evm.set_balance(bob, one_eth).unwrap(); 329 | //assert!(evm.get_balance(bob).unwrap() == one_eth); 330 | } 331 | 332 | #[test] 333 | fn simple_transfers() { 334 | let one_eth = U256::from(1e18); 335 | //let addresses = generate_random_addresses(2); 336 | let bob = Address::repeat_byte(23); 337 | let alice = Address::repeat_byte(24); 338 | 339 | let mut evm = BaseEvm::new(None); 340 | evm.create_account(bob, Some(U256::from(2e18))).unwrap(); 341 | evm.create_account(alice, None).unwrap(); 342 | 343 | assert!(evm.transfer(alice, bob, one_eth).is_err()); // alice has nothing to transfer...yet 344 | assert!(evm.transfer(bob, alice, one_eth).is_ok()); 345 | 346 | assert!(evm.get_balance(bob).unwrap() == one_eth); 347 | assert!(evm.get_balance(alice).unwrap() == one_eth); 348 | 349 | let s = evm.create_snapshot(); 350 | println!("{:?}", s); 351 | } 352 | 353 | #[test] 354 | fn no_sol_test_contract() { 355 | let contract_bytecode = hex::decode(BYTECODE).expect("failed to decode bytecode"); 356 | 357 | let zero = U256::from(0); 358 | let owner = Address::repeat_byte(12); 359 | let mut evm = BaseEvm::default(); 360 | evm.create_account(owner, Some(U256::from(1e18))).unwrap(); 361 | 362 | let mut test_contract_abi = ContractAbi::from_human_readable(vec![ 363 | "constructor(uint256)", 364 | "function owner() (address)", 365 | "function value() (uint256)", 366 | "function increment() (uint256)", 367 | "function increment(uint256) (uint256, uint256)", 368 | ]); 369 | test_contract_abi.bytecode = Some(contract_bytecode.into()); 370 | 371 | let (args, _) = test_contract_abi.encode_constructor("(1)").unwrap(); 372 | let contract_address = evm.deploy(owner, args, U256::from(0)).unwrap(); 373 | 374 | // Check owner call 375 | let (enc_owner_call, _, de1) = test_contract_abi.encode_function("owner", "()").unwrap(); 376 | let o1 = evm 377 | .transact_call(contract_address, enc_owner_call, zero) 378 | .unwrap(); 379 | assert!(DynSolValue::Address(owner) == de1.unwrap().abi_decode(&o1.result).unwrap()); 380 | 381 | // do increment() 382 | let (enc_inc_0, _, de2) = test_contract_abi 383 | .encode_function("increment", "()") 384 | .unwrap(); 385 | let o2 = evm 386 | .transact_commit(owner, contract_address, enc_inc_0, zero) 387 | .unwrap(); 388 | assert!( 389 | DynSolValue::Uint(U256::from(1), 256) == de2.unwrap().abi_decode(&o2.result).unwrap() 390 | ); 391 | 392 | // check the value 393 | let (enc_value_call, _, de3) = test_contract_abi.encode_function("value", "()").unwrap(); 394 | let o3 = evm 395 | .transact_call(contract_address, enc_value_call, zero) 396 | .unwrap(); 397 | assert!( 398 | DynSolValue::Uint(U256::from(2), 256) == de3.unwrap().abi_decode(&o3.result).unwrap() 399 | ); 400 | 401 | // do increment(value) 402 | let (enc_inc_1, _, de4) = test_contract_abi 403 | .encode_function("increment", "(2)") 404 | .unwrap(); 405 | let o4 = evm 406 | .transact_commit(owner, contract_address, enc_inc_1, zero) 407 | .unwrap(); 408 | assert!( 409 | DynSolValue::Tuple(vec![ 410 | DynSolValue::Uint(U256::from(2), 256), 411 | DynSolValue::Uint(U256::from(4), 256) 412 | ]) == de4.unwrap().abi_decode(&o4.result).unwrap() 413 | ); 414 | 415 | // simulate increment 416 | let (enc_inc_sim, _, des) = test_contract_abi 417 | .encode_function("increment", "()") 418 | .unwrap(); 419 | let os = evm 420 | .simulate(owner, contract_address, enc_inc_sim, zero) 421 | .unwrap(); 422 | assert!( 423 | DynSolValue::Uint(U256::from(4), 256) == des.unwrap().abi_decode(&os.result).unwrap() 424 | ); 425 | 426 | // make sure value didn't change from 'simulate' 427 | let (enc_value_call1, _, de5) = test_contract_abi.encode_function("value", "()").unwrap(); 428 | let o5 = evm 429 | .transact_call(contract_address, enc_value_call1, zero) 430 | .unwrap(); 431 | assert!( 432 | DynSolValue::Uint(U256::from(4), 256) == de5.unwrap().abi_decode(&o5.result).unwrap() 433 | ); 434 | } 435 | } 436 | --------------------------------------------------------------------------------