Simular is a Python API you can use to deploy and interact with Ethereum smart contracts and an embedded Ethereum Virtual Machine (EVM). It creates a Python wrapper around production grade Rust based Ethereum APIs making it very fast.

How is it different than Brownie, Ganache, Anvil?

  • It's only an EVM. It doesn't include blocks and mining
  • No HTTP/JSON-RPC. You talk directly to the EVM (and it's fast)
  • Full functionality: account transfers, contract interaction, and more.

The primary motivation for this work is to be able to model smart contract interaction in an Agent Based Modeling environment like Mesa.

Features

  • EVM: run a local version with an in-memory database, or fork db state from a remote node.
  • Snapshot: dump the current state of the EVM to json for future use in pre-populating EVM storage
  • ABI: parse compiled Solidity json files or define a specific set of functions using human-readable notation
  • Contract/Utilities: high-level, user-friendy Python API

Standing on the shoulders of giants...

Thanks to the following projects for making this work possible!

Getting Started

Simular can be installed via PyPi. It requires a Python version of >=3.11.

Install:

pip install simular-evm

Examples

Here are a few quick examples that demonstrate the API. You can find more details on the API in the Reference Guide section.

Transfer Ether between accounts

In this example, we'll create 2 Ethereum accounts and show how to transfer Ether between the accounts.


# We use this to convert Ether to Wei
from eth_utils import to_wei
# import the EVM engine and a helper function to create accounts
from simular import PyEvmLocal, create_account

# Create the EVM
evm = PyEvmLocal()

# Create 2 accounts in the EVM:

# Bob is gifted with an initial balance of 2 Ether
bob = create_account(evm, value=2)
# Alice has an account with a 0 balance
alice = create_account(evm)

# Confirm their initial balances
assert evm.get_balance(bob) == to_wei(2, "ether")
assert evm.get_balance(alice) == 0

# transfer 1 ether from Bob to Alice
evm.transfer(bob, alice, to_wei(1, "ether"))

# Check balance. Both have 1 Ether now
assert evm.get_balance(bob) == to_wei(1, "ether")
assert evm.get_balance(alice) == to_wei(1, "ether")

Deploy and interact with a Contract

Here's how you can deploy and interact with a simple Contract

For this example, we'll use the following smart contract:

// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.13;

contract SimpleCounter {
    // Store a number value in EVM storage.
    // You can read this value through the
    // auto-generated method 'number()'
    uint256 public number;

    // Increment number by the given 'num'.
    // Returns true if num is > 0 else false
    function increment(uint256 num) public returns (bool) {
        if (num > 0) {
            number += num;
            return true;
        }
        return false;
    }
}

The contract defines 2 methods we can call: increment and number. The compiled version of this contract will result in a JSON file that contains both the ABI definition of the functions and the compiled bytecode we'll use to deploy the contract to our EVM. In the example below, we assume the JSON is stored in the file counter.json.

You can learn more about ABI and the JSON format here: Solidity ABI. Tools like Foundry automatically generate the JSON file when building the code.


# import the EVM engine and a 2 helper functions to 
# create accounts and create a Contract object from the JSON file
from simular import (
   PyEvmLocal, 
   create_account, 
   contract_from_raw_abi,
)

# load the contract information from the counter.json file
with open('counter.json') as f:
   abi = f.read()

# Create the EVM
evm = PyEvmLocal()

# Create an account to deploy the contract
bob = create_account(evm)

# parses the JSON file and creates a contract 
# object with the contract functions
counter = contract_from_raw_abi(evm, abi)

# deploy the contract. returns the address of the deployed contract
address = counter.deploy(caller=bob)

# interact with the contract 

# calls a write operation (transact) incrementing number by 1
assert counter.increment.transact(1, caller=bob)

# check the value (read) of number in the contract by using 'call'
assert 1 == counter.number.call()

See Reference Guide for more API details.

Contract API

Provides a wrapper around PyEvm and PyAbi and is the easiest way to interact with Contracts. Solidity contract methods are extracted from the ABI and made available as attributes on the instance of the Contract.

For example, if a Solidity contract defines the following methods:

function hello(address caller) public returns (bool){}

function world(string name) view (string) 

they will be automatically available in the instance of the Contract like this:

# write call to the hello method
contract.hello.transact("0x11", caller="0x..")

# read call to world method
contract.world.call("dave")

Each method name is an attribute on the instance of Contract. To invoke them, you need to append either:

# this is a write/transaction
# where:
# - args: is 0 of more expected arguments to the method
# - caller: is the address of the account calling the method
# - value: is an optional value in Ether 
.transact(*args, caller: str, value: int=0)

# this is a read (view) call
# where:
# - args: is 0 of more expected arguments to the method
.call(*args)

Under the covers, a Contract knows how to properly encode all interactions with the EVM, and likewise decode any return values.

Constructor

Create an instance of a Contract from an ABI.

See Utilities for a simpler way to create a Contract

def __init__(self, evm: PyEvmLocal | PyEvmFork, abi: PyAbi)

Parameters

  • evm an instance of one of the EVMs
  • abi an instance of PyAbi

Returns self (Contract)

Example:

evm = PyEvmLocal()
abi = PyAbi.load_from_json(...)
counter = Contract(evm, abi)

Methods

at

Set the contract address. Note: this is automatically set when using deploy

def at(self, address: str) -> Contract

Parameters

  • address the address of the Contract in the EVM

Returns self

Example:

contract.at('0x11...')

deploy

Deploy the contract, returning it's address

def deploy(self, caller: str, args=[], value: int = 0) -> str

Parameters

  • caller: the address of the requester...msg.sender
  • args: a list of args expected by the Contract's constructor (if any)
  • value: optional amount of Ether for the contract

Returns the address of the deployed contract

Example:

# deploy a contract that's expecting 
# no constructor arguments and no initial balance
contract.deploy(caller='0x11...')

Utility Functions

Utilities defines several 'helper' functions to create contracts and accounts.

Functions

generate_random address

Create a random Ethereum address

def generate_random_address() -> str

Returns: an address

Example:

address = generate_random_address()

create_account

Create an account in the EVM

def create_account(
                  evm: PyEvmLocal | PyEvmFork,
                  address: str = None
                  value: int = 0) -> str

Parameters:

  • evm: PyEvmLocal | PyEvmForm. the EVM client
  • address: str optional. if set it will be used for the account address. Otherwise a random address will be generated.
  • value : int optional. create an initial balance for the account in ether

Returns: the address

Example:

evm = PyEvmLocal()
bob = create_address(evm)

create_many_accounts

Create many accounts in the EVM. Address are randomly generated

def create_many_account(
                  evm: PyEvmLocal | PyEvmFork,
                  num: int
                  value: int
                  value: int = 0) -> typing.List[str]

Parameters

  • evm: PyEvmLocal | PyEvmForm. the EVM client
  • num: int. the number of accounts to create.
  • value : int optional. create an initial balance for each account in ether

Returns a list of addresses

Example:

evm = PyEvmLocal()
# create 2 accounts, each with a balance of 1 Ether
[bob, alice] = create_many_address(evm, 2, 1)

contract_from_raw_abi

Create the contract given the full ABI. Full ABI should include abi and bytecode. This is usually a single json file from a compiled Solidity contract.

def contract_from_raw_abi(
                          evm: evm: PyEvmLocal | PyEvmFork
                          raw_abi: str) -> Contract

Parameters

  • evm : PyEvmLocal | PyEvmForm. the EVM client
  • raw_abi : abi file as un-parsed json

Returns an instance of Contract

Example:

evm = PyEvmLocal()
with open('counter.json') as f:
    raw = f.read()

contract = contract_from_raw_abi(evm, raw)

contract_from_abi_bytecode

Create a contract given the abi and bytecode.

def contract_from_abi_bytecode(
                               evm: evm: PyEvmLocal | PyEvmFork
                               raw_abi: str, 
                               bytecode: bytes) -> Contract
)

Parameters

  • evm : PyEvmLocal | PyEvmForm. the EVM client
  • raw_abi : abi file as un-parsed json
  • bytecode: bytes

Returns an instance of Contract

Example:

evm = PyEvmLocal()
with open('counter.abi') as f:
    abi = f.read()

with open('counter.bin') as f:
    bytecode = f.read()

bits = bytes.fromhex(bytecode)
contract = contract_from_abi_bytecode(abi, bits)

contract_from_inline_abi

Create the contract using inline ABI method definitions.

def contract_from_inline_abi(
                             evm: evm: PyEvmLocal | PyEvmFork)
                             abi: typing.List[str]) -> Contract

Function are described in the format: function NAME(PARAMETER TYPES) (RETURN TYPES)

where:

  • NAME if the function name
  • PARAMETER TYPES are 0 or more solidity types of any arguments to the function
  • RETURN TYPES are any expected returned solidity types. If the function does not return anything, this is not needed.

Examples:

  • "function hello(uint256,uint256)" is hello function the expects 2 int arguments and returns nothing
  • "function hello()(uint256)" is a hello function with no arguments and return an int

Parameters

  • evm : PyEvmLocal | PyEvmForm. the EVM client
  • abi : a list of strs

Returns an instance of Contract

Example:

evm = PyEvmLocal()
abi = ['function hello()(uint256)', 'function world(string) (string)']
contract = contract_from_inline_abi(evm, abi)

PyEvm API

There are 2 version of PyEvm: PyEvmLocal and PyEvmFork. They primarily differ in how they populate the EVM with state information.

PyEvmLocal uses a local in-memory datasource. It populates EVM state from user-defined interaction with the Evm (contracts, etc...).

PyEvmFork works very much the same, but has the ability to pull EVM state from a remote node. It can access on-chain state information from any available Ethereum node offering a json-rpc endpoint.

Both versions are a Python class wrapper of the REVM Rust library.

Import

from simular import PyEvmLocal, PyEvmFork

Constructor

Create an instance of the EVM with in-memory storage.

PyEvmLocal()

Create an instance of the EVM with in-memory storage and the ability to pull state from a remote node.

PyEvmFork(url: str) :

Parameters:

  • url : The HTTP address of an Ethereum json-rpc endpoint. Services such as Infura and Alchemy provide access to json-rpc endpoints

Common methods

Both versions share the following methods:

get_balance

Get the balance of the given account.

def get_balance(self, address: str) -> int

Parameters:

  • address: the address of account

Returns: int: the balance

Example:

evm = PyEvmLocal()
bal = evm.get_balance('0x123...')

create_account

Create an account in the EVM

def create_account(self, address: str, amount: int | None)

Parameters:

  • address: the address of the account to make
  • amount: (optional) the value in Ether to fund the account

Example:

evm = PyEvmLocal()
evm.create_account('0x111...', to_wei(2, 'ether'))

transfer

Transfer Ether from one account to another.

def transfer(self, caller: str, to: str, amount: int)

Parameters:

  • caller: the sender (from)
  • to: the recipient (to)
  • amount: in Ether to transfer

Example:

evm = PyEvmLocal()
evm.transfer('0x11...', '0x22...', to_wei(1, 'ether')

deploy

Deploy a contract

def deploy(self, from: str, bytecode: bytes, value: int) -> str

This is usually not called directly as it requires properly formatting bytecode. See the Contract API as an easier way to deploy a contract.

Parameters:

  • address: the account deploying the contract (from)
  • bytecode: property formatted bytes to deploy the contract in the EVM
  • value: in Ether to transfer to the contract

Returns: str: the address of the deployed contract

Example:

evm = PyEvmLocal()
evm.deploy('0x11..., b'320...', 0)

transact

Make a write operation changing the state of the given contract.

def transact(self, 
             caller: str, 
             to: str, 
             data: bytes, 
             value: int) -> (bytes, int)

This is usually not called directly as it requires properly formatting data. See the Contract API as an easier way to use transact.

Parameters:

  • caller: from address (msg.sender)
  • to: the address of the contract
  • data: abi encoded function call
  • value: in Ether to transfer to the contract

Returns: tuple: (encoded response, gas used)

Example:

evm = PyEvmLocal()
evm.transact('0x11..', '0x22..', b'661..', 0)

call

Make a read-only call to the contract

def call(self, to: str, data: bytes) -> (bytes, uint)

This is usually not called directly as it requires properly formatting data. See the Contract API as an easier way to use call.

Parameters:

  • to: the address of the contract
  • data: abi encoded function call

Returns: tuple: (encoded response, gas used)

Example:

evm = PyEvmLocal()
evm.call('0x11..', b'661..')

dump_state

Export the current state (snapshot) of the EVM to a JSON encoded string

def dump_state(self) -> str

Returns: str: JSON encoded str

Example:

evm = PyEvmLocal()
state = evm.dump_state()

view_storage_slot

View the storage slot of the given account.

def view_storage_slot(self, address: str, index: int) -> bytes

Parameters:

  • address: the address of the contract
  • index: this index of the slot in the internal Map.

Returns: bytes: encoded value. Decoding requires knowlege of the type stored in the given slot

Example:

evm = PyEvmLocal()
value = evm.view_storage_slot('0x11...', 1)

PyEVMLocal

PyEVMLocal has one additional method:

load_state

Load state into the EVM from a snapshot. See dump_state.

def load_state(self, snapshot: str)

Parameters:

  • snapshot: the json file produced by dump_state()

Example:

evm = PyEvmLocal()

with open('snapshot.json') as f:
  snap = f.read()

evm.dump_state(snap)

PyAbi API

Provides the ability to load and parse ABI files. It's primarily used to help extract the information needed to interact with smart contracts. This is rarely used directly. See Contracts and Utilities.

Import

from simular import PyAbi

Static functions

You can create an instance of PyAbi by using one of the following static methods:

load_from_json

Create an instance by loading a JSON file from a compiled Solidity contract. Expects a JSON file the includes an abi and bytecode entry.

def load_from_json(abi: str) -> self

Parameters:

  • abi: an un-parsed json encoded file

Returns: an instance of PyAbi

Example:

with open('counter.json') as f:
    raw = f.read()
abi = PyAbi.load_from_json(raw)

load_from_parts

Create an instance by loading a the json encoded abi information and contract bytecode.

def load_from_parts(abi: str, bytecode: bytes) -> self

Parameters:

  • abi: an un-parsed json encoded file with just the abi information
  • bytecode: the contract bytecode

Returns: an instance of PyAbi

Example:

with open('counter.abi') as f:
    abi = f.read()

with open('counter.bin') as f:
    bytecode = f.read()

bits = bytes.fromhex(bytecode)
abi = PyAbi.load_from_json(abi, bits)

load_from_human_readable

Create an instance from a list of contract function descriptions

def load_from_human_readable(items: typing.List[str]) -> self

Parameters:

  • items: is a list of function desciptions

Returns: an instance of PyAbi

A function description is shorthand way of describing the function name, inputs, and outputs. The format is the form:

function NAME(ARG TYPES) (RETURN TYPES) Where:

  • NAME: is the name of the function.
  • ARG TYPES: 0 or more of the require Solidity input types
  • RETURN TYPES: tuple of expected Solidity return types. This is not required if the function doesn't return anything.

For example:

'function hello() (uint256)' is a solidity function named hello that takes no input arguments and returns an uint256

'function hello(address, uint256)' is a solidity function named hello that takes 2 arguments, an address, and uint256 and returns nothing.

Example:

abi = PyAbi.load_from_human_readable([
    'function hello() (uint256)', 
    'function hello(address, uint256)'])

Methods

has_function

Does the ABI include the function with the given name.

def has_function(self, name: str) -> bool

Parameters:

  • items: is a list of function desciptions

Returns: an instance of True | False

Example

with open('counter.json') as f:
    raw = f.read()
abi = PyAbi.load_from_json(raw)

assert abi.has_function("increment")

has_fallback

Does the Contract define a fallback function

def has_fallback(self) -> bool

Returns: an instance of True | False

Example

with open('counter.json') as f:
    raw = f.read()
abi = PyAbi.load_from_json(raw)

assert not abi.has_fallback()

has_receive

Does the Contract define a receive function

def has_receive(self) -> bool

Returns: an instance of True | False

Example

with open('counter.json') as f:
    raw = f.read()
abi = PyAbi.load_from_json(raw)

assert not abi.has_receive()

bytecode

Return the byte from the ABI

def bytecode(self) -> bytes | None

Returns: bytes or None if the bytecode wasn't set

with open('counter.json') as f:
    raw = f.read()
abi = PyAbi.load_from_json(raw)

abi.bytecode()

constructor_input_types

Return a list (if any) of the exopected constructor arguments.

def constructor_input_types(self) -> typing.List[str] | None

Returns: a list of the Solidity types expected as arguments to the constructor. Or None if the constructor doesn't take any arguments.

with open('counter.json') as f:
    raw = f.read()
abi = PyAbi.load_from_json(raw)

abi.constructor_input_types()

encode_function_input

Encode the function with any given input to call on the EVM. See Function Encoding for more details.

def encode_function_input(self, 
                          name: str, 
                          args: typing.Tuple[Any]
) -> typing.Tuple(bytes, typing.List[str])

Parameters:

  • name: of the contract method to encode
  • args: 0 or more arguments to pass to the method

Returns: tuple: the encoded bytes and a list of the expected output types from calling the method

with open('counter.json') as f:
    raw = f.read()
abi = PyAbi.load_from_json(raw)

abi.encode_function_input(self, "increment", (1,))