Eclair: a Solidity Interpreter

Eclair is a Solidity interpreter designed to provide a fast and intuitive REPL to interact with EVM smart contracts using Solidity.

Eclair is built on top of the Solang for parsing and Alloy for most Ethereum related features.

Eclair is designed to integrate well with the rest of the ecosystem, and can be used seamlessly with Foundry, Hardhat and Brownie on any EVM-compatible chain.

Although Eclair uses Solidity as a base for its syntax, the semantics are not designed to be exactly the same as those provided by the Solidity compiler. In most cases, ease-of-use and simplicity are prioritized over compatibility with Solidity.

Here is a short example of an interaction using Eclair to interact with smart contracts deployed on Optimism using a Ledger.

>> vm.rpc("https://mainnet.optimism.io")
>> abi.fetch("ERC20", 0xded3b9a8dbedc2f9cb725b55d0e686a81e6d06dc)
ERC20(0xdEd3b9a8DBeDC2F9CB725B55d0E686A81E6d06dC)
>> usdc = ERC20(0x0b2c639c533813f4aa9d7837caf62653d097ff85)
>> accounts.loadLedger(5)
0x2Ed58a93c5Daf1f7D8a8b2eF3E9024CB6BFa9a77
>> usdc.balanceOf(accounts.current).format(usdc.decimals())
"5.00"
>> swapper = abi.fetch("Swapper", 0x956f9d69Bae4dACad99fF5118b3BEDe0EED2abA2)
>> usdc.approve(swapper, 2e6)
Transaction(0xed2cfee9d712fcaeb0bf42f98e45d09d9b3626a0ee93dfc730a3fb7a0cda8ff0)
>> targetAsset = abi.fetch("LToken", 0xc013551a4c84bbcec4f75dbb8a45a444e2e9bbe7)
>> amountIn = 2e6
>> minOut = 2e18.div(targetAsset.exchangeRate()).mul(0.95e18)
>> minOut.format(targetAsset.decimals())
"4.53"
>> swapper.mint
Swapper(0x956f9d69Bae4dACad99fF5118b3BEDe0EED2abA2).mint(address zapAssetAddress_,
  address leveragedTokenAddress_,uint256 zapAssetAmountIn_,uint256 minLeveragedTokenAmountOut_)
>> tx = swapper.mint(usdc, targetAsset, amountIn, minOut)
>> receipt = tx.getReceipt()
>> receipt.tx_hash
0xbdbaddb66c696afa584ef93d0d874fcba090e344aa104f199ecb682717009691
>> receipt.gas_used
1803959

Note that this requires Optimism Etherscan API key to be set.

Project status

Eclair is still in its early stages of development but is already functional enough for many common blockchain tasks and interactions.

Below is a non-exhaustive list of features that are planned:

  • Improved call traces
  • Support for overloaded functions and events

Some time also needs to be spent on improving performance and memory usage but it is not a priority for now.

Feature requests are very welcome but please provide a clear use case and context for the feature you are requesting. Contributions are also more than welcome.

Installation

Requirements

Eclair requires libusb to work, since it is used to communicate with the Ledger device. On macOS, you can install it using Homebrew:

brew install libusb

On Linux, you should be able to install it using your package manager, for example on Ubuntu:

sudo apt install libusb-1.0-0-dev

Using the installer

You can install the latest version of Eclair (latest push on main branch) using the installer script:

curl -L https://install.eclair.so | bash

This will install Eclair in the Foundry bin directory, typically ~/.foundry/bin.

If you want the latest published release instead, you can use the following command:

curl -L https://install.eclair.so | bash -s -- --version release

To install a specific version, you can use the following command:

curl -L https://install.eclair.so | bash -s -- --version VERSION

where VERSION should be replaced with a release version.

Installing from binaries

The latest release binaries are available at:

Just download the binary, make it executable, and preferably put it in a directory on the path.

The release binaries can be found on the releases page.

Installing the latest version from source

This requires to have Rust and Cargo installed. If you don't have it, you can install it by following the instructions on the official website.

cargo install --git https://github.com/danhper/eclair.git eclair

Configuration

Eclair can be used without any configuration but it is possible to configure it to make the experience smoother. Eclair will also load Foundry's configuration which can be used for some settings such as the RPC URL and the Etherscan API key. Using the global configuration at ~/.foundry/foundry.toml can be convenient for such settings.

A sample Foundry configuration file can be found in the config directory.

RPC URL

The RPC url can be configured in several ways:

  • Using the ETH_RPC_URL environment variable
  • Using the --rpc-url option in the command line
  • Using the vm.rpc(RPC_URL) function inside a session

Using Foundry configuration file

The rpc_endpoints section of foundry.toml to set aliases for your RPC URLs. Here is a sample configuration:

# ~/.foundry/foundry.toml
[rpc_endpoints]
mainnet = "https://eth.llamarpc.com"
optimism = "https://mainnet.optimism.io"

This then allows to use vm.rpc("mainnet") or vm.rpc("optimism") to connect to the respective networks.

Etherscan API Key

Eclair requires an Etherscan API key to fetch contract ABIs and interact with the Etherscan API. The API key can be set using the ETHERSCAN_API_KEY environment variable, which will be available for all chains. For a per-chain configuration, the following environment variables can be used:

  • Ethereum: ETHERSCAN_API_KEY
  • Optimism: OP_ETHERSCAN_API_KEY
  • Gnosis Chain: GNOSISSCAN_API_KEY
  • Polygon: POLYGONSCAN_API_KEY
  • Polygon zkEVM: POLYGONSCAN_ZKEVM_API_KEY
  • Base: BASESCAN_API_KEY
  • Arbitrum: ARBISCAN_API_KEY
  • Sepolia: SEPOLIA_ETHERSCAN_API_KEY

Using Foundry configuration file

The etherscan section of foundry.toml can be used to set the API key for different chains.

Here is a sample configuration:

# ~/.foundry/foundry.toml
[etherscan]
mainnet = { key = "ETHERSCAN_API_KEY" }
optimism = { key = "OP_ETHERSCAN_API_KEY" }

Initial setup

To allow to load common contracts and perform any other setup, Eclair can be configured to run code at startup. By default, Eclair will look for a file named .eclair_init.sol in the current directory as well as in the $HOME/.foundry directory. Eclair will look for a function called setUp and load everything defined there in the current environment. Note that any function defined will be loaded as normal functions and can be called from the REPL.

Here is an example setup:

function setUp() {
    abi.load("ERC20", "~/.foundry/abis/erc20.json");

    if (!vm.connected) return;

    chainid = block.chainid;
    if (chainid == 1) {
        usdc = ERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
    } else if (chainid == 10) {
        usdc = ERC20(0x0b2c639c533813f4aa9d7837caf62653d097ff85);
    }
}

Account management

Loading an account is needed to send transactions to smart contract. Eclair currently supports loading an account using a private key or a Ledger device.

Loading an account

Using a private key

Accounts can be loaded from a private key, using the accounts.loadPrivateKey function:

>> accounts.loadPrivateKey() // usage with no argument
Enter private key:
0xec1a77322592C180Ca626C2f9DDe3976E5712011
>> accounts.loadPrivateKey("4546df48889621143395cf30567352fab50ed9c48149836e726550f1361e43df") // passing the private key as an argument
0xec1a77322592C180Ca626C2f9DDe3976E5712011
>> accounts.current
0xec1a77322592C180Ca626C2f9DDe3976E5712011

Using a ledger

Accounts can be read from a Ledger device, using the accounts.loadLedger function. The accounts.listLedgerWallets() function can be used to list the available wallets on the Ledger device. The index of the wallet to load should be passed as an argument to the accounts.loadLedger function. Note that only the ledger live derivation is supported for now.

>> accounts.listLedgerWallets()
[0x4d09e5617C38F8A9884d79464B7BE1b12353eD05, 0x669F44bB2DFb534707E6FAE940d7558ab0FE254D, 0x5Ac61EbcEbf7De5D19a807752f13Cd7a9Af4Ffc4, 0xb3eBAf4686741C8a7A2Adf7738A1a84a883127c2, 0xC033068376264C0a5971b706894d3fc0eB93A2dD]
>>> accounts.loadLedger(1)
0x669F44bB2DFb534707E6FAE940d7558ab0FE254D
>> accounts.current
0x669F44bB2DFb534707E6FAE940d7558ab0FE254D

Using a keystore

Accounts can be loaded from a keystore created by cast, using the accounts.loadKeystore function.

The keystore can be created using the cast wallet import command:

cast wallet import my-account --interactive

This will create a keystore file in ~/.foundry/keystore/my-account. It can then be loaded into Eclair using:

>> accounts.loadKeystore("my-account")

loadKeystore will prompt for the password to decrypt the keystore. The password can also be passed as a second argument.

Using loaded accounts

Once an account is loaded, it can be used to send transactions to smart contracts. The currently used account can be retrieved using the accounts.current property. A list of all loaded accounts can be retrieved using the accounts.loaded property.

>> accounts.loadPrivateKey()
0xec1a77322592C180Ca626C2f9DDe3976E5712011
>> accounts.loadKeystore("test")
0x669F44bB2DFb534707E6FAE940d7558ab0FE254D
>> accounts.loaded
[Account { address: 0xec1a77322592C180Ca626C2f9DDe3976E5712011, alias: null }, Account { address: 0x669F44bB2DFb534707E6FAE940d7558ab0FE254D, alias: null }]

Loaded accounts can be selected using the accounts.select function.

>> accounts.select(0xec1a77322592C180Ca626C2f9DDe3976E5712011)
0xec1a77322592C180Ca626C2f9DDe3976E5712011
>> accounts.current
0xec1a77322592C180Ca626C2f9DDe3976E5712011

Aliasing accounts

Accounts can be aliased using the accounts.alias function.

>> accounts.alias(0xec1a77322592C180Ca626C2f9DDe3976E5712011, "my-account")
>> accounts.loaded
[Account { address: 0xec1a77322592C180Ca626C2f9DDe3976E5712011, alias: "my-account" }, Account { address: 0x669F44bB2DFb534707E6FAE940d7558ab0FE254D, alias: null }]

Accounts can then be selected using the alias:

>> accounts.select("my-account")
0xec1a77322592C180Ca626C2f9DDe3976E5712011

The functions to load accounts also allow to pass an alias as an argument:

>> accounts.loadKeystore("test", "my-account")
0xec1a77322592C180Ca626C2f9DDe3976E5712011
>> accounts.loaded
[Account { address: 0xec1a77322592C180Ca626C2f9DDe3976E5712011, alias: "my-account" }]

Contracts management

Eclair provides different ways to load contracts (or rather contract ABIs) to be able to interact with them.

Loading from existing project

If eclair is ran in a directory using Foundry, Brownie or Hardhat, all the compiled contracts in the project will be loaded automatically. No additional setup is needed. A list of all loaded contracts can be viewed using repl.types. One caveat is that Eclair does not currently supports multiple contracts with the same name, so last occurrence will overwrite the previous one.

Loading from an ABI file

Contracts can be loaded from a JSON ABI file using the abi.load function. The first argument is the name of the contract, which will be defined in the environment. The second argument is the path to the JSON file containing the ABI.

abi.load("ERC20", "path/to/abi.json")

If the ABI is nested in the JSON file (e.g. under the abi key), the key can be specified as a third argument.

abi.load("ERC20", "path/to/abi.json", "abi")

Fetching ABIs from Etherscan

Contracts can be loaded from Etherscan using the abi.fetch function. Note that this requires an Etherscan API key to be set. The first argument is, as for abi.load the name of the contract, and the second argument is the address of the contract.

dai = abi.fetch("DAI", 0x6B175474E89094C44Da98b954EedeAC495271d0F)
>> dai
DAI(0x6B175474E89094C44Da98b954EedeAC495271d0F)

For contracts that use a proxy pattern, the process can be split into two steps.

abi.fetch("USDC", 0x43506849D7C04F9138D1A2050bbF3A0c054402dd); // implementation address
usdc = USDC(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); // proxy address

Interacting with contracts

Eclair provides a simple way to interact with smart contracts using familiar Solidity syntax.

Calling contracts and sending transactions

For any contract loaded, all the functions of the contract defined in its ABI are available as methods on the contract object.

By default, calling a view function will call it and return the result, while calling a non-view function will send a transaction and returns the transaction hash. Sending a transaction requires an account to be loaded.

The behavior can be changed by using one of the following method on the returned function object:

  • call: Call the function and return the result
  • traceCall: Same as call but also prints the trace of the call (also potentially shows better error messages)
  • send: Sends a transaction to the function and return the result
  • encode: ABI-encodes the function call
>> dai = abi.fetch("DAI", 0x6B175474E89094C44Da98b954EedeAC495271d0F)
>> dai.balanceOf(0x4DEDf26112B3Ec8eC46e7E31EA5e123490B05B8B)
49984400000000000000000000
>> vm.loadPrivateKey()
>> dai.balanceOf.send(0x4DEDf26112B3Ec8eC46e7E31EA5e123490B05B8B)
Transaction(0x6a2f1b956769d06257475d18ceeec9ee9487d91c97d36346a3cc84d568e36e5c)
>> dai.balanceOf.encode(0x4DEDf26112B3Ec8eC46e7E31EA5e123490B05B8B)
0x70a082310000000000000000000000004dedf26112b3ec8ec46e7e31ea5e123490b05b8b
>> dai.transfer(0x4DEDf26112B3Ec8eC46e7E31EA5e123490B05B8B, 1e18)
Transaction(0xf3e85039345ff864bb216b10e84c7d009e99ec55b370dae22706b0d48ea41583)

Transaction options

There are different options available when calling and sending transactions to contracts. The options can be passed using the {key: value} Solidity syntax, for example:

>> tx = weth.deposit{value: 1e18}()

The following options are currently supported:

  • value: sets the msg.value of the transaction
  • block: sets the block number to execute the call on (only works for calls, not for sending transactions)
  • from: sets the from for the call (only works for calls, not for sending transactions)
  • gasLimit: sets the gas limit to use for the transaction
  • maxFee: sets the maximum fee to pay for the transaction
  • priorityFee: sets the priority fee to pay for the transaction
  • gasPrice: sets gas price to use for the (legacy) transaction

Transaction receipts

After sending a transaction, you can get the transaction receipt using the Transaction.getReceipt method.

>> tx = dai.approve(0x83F20F44975D03b1b09e64809B757c47f942BEeA, 1e18)
>> tx.getReceipt()
TransactionReceipt { tx_hash: 0x248ad948d1e4eefc6ccb271cac2001ebbdb2346beddc7656b1f9518f216c8b02, block_hash: 0x688517fe5e540b4e3953ed3ba84cc4d70903ddffb981a66c51ca49ca13c90bb1, block_number: 20380613, status: true, gas_used: 46146, gas_price: 4547819249 }
>> _.logs
[Log { address: 0x6B175474E89094C44Da98b954EedeAC495271d0F, topics: [0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925, 0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266, 0x00000000000000000000000083f20f44975d03b1b09e64809b757c47f942beea], data: 0x0000000000000000000000000000000000000000000000000de0b6b3a7640000 }]

If the ABI of the contract emitting the log is loaded, the logs will automatically be decoded and the decoded arguments will be available in the args property of each log.

Events

Eclair provides a way to fetch events emitted by a contract using the events.fetch method.

>> events.fetch{fromBlock: 20490506, toBlock: 20490512}(0xe07F9D810a48ab5c3c914BA3cA53AF14E4491e8A)[0]
Log { address: 0xe07F9D810a48ab5c3c914BA3cA53AF14E4491e8A, topics: [0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef, 0x000000000000000000000000ba12222222228d8ba445958a75a0704d566bf2c8, 0x000000000000000000000000f081470f5c6fbccf48cc4e5b82dd926409dcdd67], data: 0x00000000000000000000000000000000000000000000000e8bd6d724bc4c7886, args: Transfer { from: 0xBA12222222228d8Ba445958a75a0704d566BF2C8, to: 0xf081470f5C6FBCCF48cC4e5B82Dd926409DcdD67, value: 268330894800999708806 } }

The events.fetch accepts either a single address or a list of addresses as the first argument, as well as some options to filter the logs returned. It returns a list of logs that match the given criteria, and automatically decodes each log if the ABI is loaded.

Options

The events.fetch method accepts the following options:

  • fromBlock: the block number to start fetching events from
  • toBlock: the block number to stop fetching events at
  • topic0: topic0 of the event
  • topic1: topic1 of the event
  • topic2: topic2 of the event
  • topic3: topic3 of the event

By default, it will try to fetch from the first ever block to the latest block. In many cases, the RPC provider will reject the request because too much data would be returned, in which case options above will need to be added to restrict the size of the response.

To only get one type of event, e.g. Transfer, you can filter using topic0 and the selector of the desired event.

>> events.fetch{fromBlock: 20490506, toBlock: 20490512, topic0: ERC20.Approval.selector}(0xe07F9D810a48ab5c3c914BA3cA53AF14E4491e8A)[0]
Log { address: 0xe07F9D810a48ab5c3c914BA3cA53AF14E4491e8A, topics: [0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925, 0x0000000000000000000000008149dc18d39fdba137e43c871e7801e7cf566d41, 0x000000000000000000000000ea50f402653c41cadbafd1f788341db7b7f37816], data: 0x000000000000000000000000000000000000000000000025f273933db5700000, args: Approval { owner: 0x8149DC18D39FDBa137E43C871e7801E7CF566D41, spender: 0xeA50f402653c41cAdbaFD1f788341dB7B7F37816, value: 700000000000000000000 } }

Decoding Data

Eclair provides several functions for decoding ABI-encoded data, which can be useful when verifying transactions.

Basic ABI Decoding

The abi.decode function works similarly to Solidity's abi.decode, allowing to decode ABI-encoded data with known types:

>> encoded = abi.encode("Hello", 123)
>> (str, num) = abi.decode(encoded, (string, uint256))
>> str
"Hello"
>> num
123

Decoding Function Call Data

For decoding function calls or error data from contracts, you can use abi.decodeData. This function requires that the relevant contract ABI has been loaded (see Contracts management):

>> dai = ERC20(0x6b175474e89094c44da98b954eedeac495271d0f)
>> data = dai.transfer.encode(0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045, 1e18)
>> abi.decodeData(data)
("transfer(address,uint256)", (0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045, 100000000000000000000))

Note that the output is a tuple and can be manipulated as such:

>> (func, args) = abi.decodeData(data)
>> func
"transfer(address,uint256)"
>> args
(0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045, 1000000000000000000)
>> args[1].format()
"1.00"

Decoding Safe MultiSend Transactions

For decoding Gnosis Safe multiSend transactions, use abi.decodeMultisend:

>> multisendData = 0x008a5eb9a5b726583a213c7e4de2403d2dfd42c8a600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044e2a4853ae060499125866d3940796528a5be3e30632cf5c956aae07e9b72d89c96e053f100000000000000000000000000000000000000000000000006f05b59d3b20000008a5eb9a5b726583a213c7e4de2403d2dfd42c8a600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044e2a4853a2a55eed44296e96ac21384858860ec77b2c3e06f2d82cbe24bc29993e5a520110000000000000000000000000000000000000000000000000de0b6b3a7640000
>> abi.decodeMultisend(multisendData)
[MultisendTransaction {
    operation: 0,
    to: 0x8A5eB9A5B726583a213c7e4de2403d2DfD42C8a6,
    value: 0,
    data: 0xe2a4853ae060499125866d3940796528a5be3e30632cf5c956aae07e9b72d89c96e053f100000000000000000000000000000000000000000000000006f05b59d3b20000
}, MultisendTransaction {
    operation: 0,
    to: 0x8A5eB9A5B726583a213c7e4de2403d2DfD42C8a6,
    value: 0,
    data: 0xe2a4853a2a55eed44296e96ac21384858860ec77b2c3e06f2d82cbe24bc29993e5a520110000000000000000000000000000000000000000000000000de0b6b3a7640000
}]

Each decoded transaction contains:

  • operation: 0 for regular call, 1 for delegatecall
  • to: Target address
  • value: ETH value in wei
  • data: Call data for the transaction

This can be combined with abi.decodeData to decode the data of each transaction. For example:

>> abi.decodeMultisend(multisendData).map((d) >> (d, abi.decodeData(d.data)))
[(MultisendTransaction {
    operation: 0,
    to: 0x8A5eB9A5B726583a213c7e4de2403d2DfD42C8a6,
    value: 0,
    data: 0xe2a4853ae060499125866d3940796528a5be3e30632cf5c956aae07e9b72d89c96e053f100000000000000000000000000000000000000000000000006f05b59d3b20000
}, ("setUint(bytes32,uint256)", (0xe060499125866d3940796528a5be3e30632cf5c956aae07e9b72d89c96e053f1, 500000000000000000))
), (MultisendTransaction {
    operation: 0,
    to: 0x8A5eB9A5B726583a213c7e4de2403d2DfD42C8a6,
    value: 0,
    data: 0xe2a4853a2a55eed44296e96ac21384858860ec77b2c3e06f2d82cbe24bc29993e5a520110000000000000000000000000000000000000000000000000de0b6b3a7640000
}, ("setUint(bytes32,uint256)", (0x2a55eed44296e96ac21384858860ec77b2c3e06f2d82cbe24bc29993e5a52011, 1000000000000000000))
)]

Differences with Solidity

Eclair is based on the Solidity syntax but behaves differently in many aspects. It is worth nothing that at this point, Eclair only implements a subset of the Solidity language.

This section highlights some of the differences between Eclair and Solidity.

Static vs dynamic typing

Solidity is statically typed, meaning that the type of each variable must be known at compile time and the compiler will check that the types are used correctly. On the other hand, Eclair is dynamically typed and variables are only checked at runtime. Furthermore, variables do not need to be declared explicitly.

>> a = 1; // a is an integer
>> a + 2
3
>> a + "bar"
Error: cannot add uint256 and string
>> a = "foo"; // a is now a string
>> a + "bar"
"foobar"

Scopes

Eclair has a different scoping mechanism compared to Solidity. In particular, Eclair only creates a new scope for function, not for blocks. This means that the following code will work in Eclair but not in Solidity:

if (true) {
    a = 1;
} else {
    a = 2;
}
console.log(a);

However, the following code will not work:

>> function foo() { a = 1; }
>> console.log(a)
Error: a is not defined

First-class functions and types

Internally, almost everything in Eclair returns a value. This means that functions and types can be passed around as arguments to other functions. For example, the following is valid in Eclair:

>> usdc = ERC20(0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48)
>> usdcBalance = usdc.balanceOf
>> usdcBalance(0x47ac0Fb4F2D84898e4D9E7b4DaB3C24507a6D503)
542999999840000
>> tu16 = type(uint16)
>> tu16.max
65535

Anonymous functions

Eclair supports anonymous functions, which are functions that are not bound to a name. These functions are quite limited in features since they need to be written as a valid Solidity expression. We adopted the following syntax:

>> (x) >> x + 1
function(any x)

This is particularly helpful when using higher-order functions like map and filter:

>> [1, 2, 3].map((x) >> x + 1)
[2, 3, 4]
>> [1, 2, 3].filter((x) >> (x % 2 == 0))
[2]

Note that because >> is parsed as the right shift operator, you need to wrap the anonymous function in parentheses if it contains an operator with a lower priority than >>, such as ==.

Comparison with other REPLs

There are already several REPLs available for Solidity, such as chisel or solidity-shell. These REPLs provide a full Solidity environment by executing the code in the EVM, which makes them very useful tools to use a playground for Solidity.

Eclair has a different goal: it tries to make contract interactions as easy as possible while keeping the Solidity syntax. To make this possible, it uses a very different approach: it does not execute the code in the EVM, but instead it interprets the code and generates the required transactions to interact with the contract on the fly.

As a byproduct of this approach, Eclair is able to provide features that would otherwise be hard or impossible to implement in a full EVM environment, such as having access to transaction receipts. It also makes the execution speed much faster, which is nice in an interactive environment, as it does not need to compile and execute the code in the EVM.

Builtin values

Globals

_

The underscore _ variable stores the result of the last expression evaluated.

>> 1 + 1
2
>> _
2

keccak256(bytes data) -> bytes32

Returns the keccak256 hash of the input bytes.

>> keccak256("hello")
0x1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8

type(any value) -> Type

Returns the type of the input value.

>> type(1)
uint256
>> type(uint256)
type(uint256)

format(any value, uint8? decimals, uint8? precision) -> string

Returns a human-readable (when possible) representation of the input value. The decimals parameter is used to specify the decimals scaling factor for the value and the precision parameter is used to specify the number of decimal places to display. These are only relevant for numerical values.

>> format(2.54321e18)
"2.54"
>> format(2.54321e18, 18, 3)
"2.543"
>> format(2.54321e7, 6)
"25.43"
>> format(bytes32("foo"))
"foo"

repl functions

repl.vars -> null

Displays a list of the variables defined in the current session.

repl.types -> null

Displays a list of the types (excluding builtins).

repl.exec(string command) -> uint256

Executes a command in the shell, displays the output and returns the exit code.

>> code = repl.exec("cat ./foundry.toml")
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
>> code
0

accounts functions

accounts.current -> address | null

Returns the currently loaded account or null if none.

accounts.loadPrivateKey((string | null)? privateKey, string? alias) -> address

Sets the current account to the one corresponding to the provided private key and returns the loaded address. If no private key is provided, the user will be prompted to enter one.

accounts.listKeystores() -> string[]

Returns a list of all keystores in ~/.foundry/keystore/.

accounts.loadKeystore(string name, string? alias, string? password) -> address

Loads the keystore located in ~/.foundry/keystore/<name> and sets the current account to the one corresponding to the keystore. If the password is not provided as the second argument, it will be prompted.

See Using a keystore for more information.

accounts.listLedgerWallets(uint256 count) -> address[]

Returns a list of count wallets in the ledger.

accounts.loadLedger(uint256 index, string? alias) -> address

Sets the current account to the one at the given ledger index and returns the loaded address. The index should match the order of the wallets returned by accounts.listLedgerWallets, starting at 0. Only the ledger live derivation path is supported.

accounts.alias(address account, string alias) -> null

Sets the alias for the given account.

accounts.loaded -> Account[]

Returns a list of all loaded accounts.

accounts.select(address account) | accounts.select(string alias) -> address

Sets the current account to the one at the given address or alias and returns the loaded address.

vm functions

vm.connected -> bool

Returns true if the session is connected to a valid RPC.

vm.rpc() -> string

Returns the URL of the RPC the REPL is connected to.

>> vm.rpc()
"https://mainnet.optimism.io/"

vm.rpc(string url) -> null

Sets the URL of the RPC to use.

>> vm.rpc("https://mainnet.optimism.io/")

If the RPC URL is set in the configuration file, the argument can be the name of the alias instead of the full URL:

>> vm.rpc("optimism")

vm.fork() | vm.fork(string url) -> string

This creates a fork using Anvil. If a URL is provided, it will fork that network, otherwise it will use the current RPC (vm.rpc()). This returns the endpoint of the Anvil instance.

>> vm.fork()
"http://localhost:54383/"

vm.startPrank(address account) -> address

Starts a prank on the given account. This only works if connected to an RPC that supports anvil_impersonateAccount (which is the case after calling vm.fork()).

>> vm.startPrank(0xCdaa941eB36344c54139CB9d6337Bd2154BBeEfA)
0xCdaa941eB36344c54139CB9d6337Bd2154BBeEfA

vm.stopPrank()

Stops the current prank.

>> vm.stopPrank()

vm.deal(address account, uint256 balance)

Sets the ETH balance of account to balance. This only works if connected to an RPC that supports anvil_setBalance (which is the case after calling vm.fork()).

>> vm.deal(0xCdaa941eB36344c54139CB9d6337Bd2154BBeEfA, 1e18)

vm.block() -> uint256 | string

Returns the current block in use for contract calls.

>> vm.block()
"latest"

NOTE: this is different from block.number which returns the current block number of the chain.

vm.block(uint256 number) | vm.block(string tag) | vm.block(bytes32 hash)

Sets the block to use for contract calls. Can be a number, a tag (e.g. "latest" or "safe"), or a block hash.

>> vm.block(123436578)
>> vm.block()
123436578

vm.mine() | vm.mine(uint256 blocks)

Mines 1 block with no arguments or otherwise blocks blocks. NOTE: This only works for Anvil RPC endpoints.

>> vm.mine(3)

vm.skip(uint256 seconds)

Skips seconds seconds in the blockchain. NOTE: This only works for Anvil RPC endpoints. block.timestamp will be updated next time a block is mined.

>> vm.skip(3600)

console functions

console.log(any... value) -> null

Logs the values to the console.

>> console.log(1, "foo", 0x6B175474E89094C44Da98b954EedeAC495271d0F)
1
"foo"
0x6B175474E89094C44Da98b954EedeAC495271d0F

json functions

json.stringify(any value) -> string

Converts the value to a JSON string.

>> json.stringify((1, ["a", "b"]))
"[1,["a","b"]]"

fs functions

fs.write(string path, string value) -> void

Writes the value to the file at the given path.

>> fs.write("./file.txt", "hello")

abi functions

abi.encode(any... args) -> bytes

Encodes the arguments according to the ABI encoding rules. Behaves like the regular Solidity abi.encode function.

>> abi.encode(uint8(1), 0x789f8F7B547183Ab8E99A5e0E6D567E90e0EB03B)
0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000789f8f7b547183ab8e99a5e0e6d567e90e0eb03b

abi.encodePacked(any... args) -> bytes

Concatenates all the arguments without padding. Behaves like the regular Solidity abi.encodePacked function.

>> abi.encodePacked(uint8(1), 0x789f8F7B547183Ab8E99A5e0E6D567E90e0EB03B)
0x01789f8f7b547183ab8e99a5e0e6d567e90e0eb03b

abi.decode(bytes data, (type...)) -> any

Decodes the data according to the ABI encoding rules, given the types. Behaves like the regular Solidity abi.decode function.

>> abi.decode(0x0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000789f8f7b547183ab8e99a5e0e6d567e90e0eb03b, (uint8, address))
(1, 0x789f8F7B547183Ab8E99A5e0E6D567E90e0EB03B)

abi.decodeData(bytes data) -> any

Decodes the data (either function calldata or error data) using any registered ABI.

>> abi.decodeData(0xa9059cbb000000000000000000000000789f8f7b547183ab8e99a5e0e6d567e90e0eb03b0000000000000000000000000000000000000000000000000de0b6b3a7640000)
("transfer(address,uint256)", (0x789f8F7B547183Ab8E99A5e0E6D567E90e0EB03B, 1000000000000000000))

abi.decodeMultisend(bytes data) -> MultisendTransaction[]

Decodes a Safe multisend transaction.

>> abi.decodeMultisend(0x008a5eb9a5b726583a213c7e4de2403d2dfd42c8a600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044e2a4853ae060499125866d3940796528a5be3e30632cf5c956aae07e9b72d89c96e053f100000000000000000000000000000000000000000000000006f05b59d3b20000008a5eb9a5b726583a213c7e4de2403d2dfd42c8a600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044e2a4853a2a55eed44296e96ac21384858860ec77b2c3e06f2d82cbe24bc29993e5a520110000000000000000000000000000000000000000000000000de0b6b3a7640000)
[MultisendTransaction { operation: 0, to: 0x8A5eB9A5B726583a213c7e4de2403d2DfD42C8a6, value: 0, data: 0xe2a4853ae060499125866d3940796528a5be3e30632cf5c956aae07e9b72d89c96e053f100000000000000000000000000000000000000000000000006f05b59d3b20000 }, MultisendTransaction { operation: 0, to: 0x8A5eB9A5B726583a213c7e4de2403d2DfD42C8a6, value: 0, data: 0xe2a4853a2a55eed44296e96ac21384858860ec77b2c3e06f2d82cbe24bc29993e5a520110000000000000000000000000000000000000000000000000de0b6b3a7640000 }]

abi.load(string name, string path, string? key) -> null

Loads an ABI from a file. The name parameter is used to reference the ABI in the REPL, the path parameter is the path to the file containing the ABI, and the key parameter is used to specify the key in the JSON file if it is nested.

abi.load("ERC20", "./ERC20.json");
dai = ERC20(0x6b175474e89094c44da98b954eedeac495271d0f);

abi.load("ERC20", "./CompiledERC20.json", "abi");

abi.fetch(string name, address implementationAddress) -> string

Fetches the ABI of a contract from Etherscan using the Etherscan API key. The name parameter is used to reference the ABI in the REPL, and the implementationAddress parameter is the address of the contract. In the case of a proxy contract, the address of the implementation contract should be provided. See contract management for more information about proxy handling. See Etherscan API Key configuration for more information on how to set the API key.

>> dai = abi.fetch("DAI", 0x6B175474E89094C44Da98b954EedeAC495271d0F)

block functions

block.number -> uint256

Returns the current block number.

block.timestamp -> uint256

Returns the current block timestamp.

block.basefee -> uint256

Returns the current block base fee.

block.chainid -> uint256

Returns the current chain ID.

events functions

events.fetch{options}(address target) -> Log[] | events.fetch{options}(address[] targets) -> Log[]

Fetches the events emitted by the contract(s) at the given address(es). For more information, see events.

Builtin methods

Global methods

These methods are available on all types.

any.format() -> string

Returns a human-readable (when possible) representation of the value. See the format function for more details.

string methods

string.length -> uint256

Returns the length of the string.

>> "foo".length
3

string.concat(string other) -> string

Concatenates two strings.

>> "foo".concat("bar")
"foobar"

bytes methods

bytes.length -> uint256

Returns the length of the bytes.

bytes.concat(bytes other) -> bytes

Concatenates two byte arrays.

array methods

array.length -> uint256

Returns the length of the array.

array.map(function f) -> array

Applies the function f to each element of the array and returns a new array with the results.

>> function double(x) { return x * 2; }
>> [1, 2, 3].map(double)
[2, 4, 6]

array.filter(function p) -> array

Applies the predicate p to each element and only includes the elements for which the predicate returns true.

>> function isEven(x) { return x % 2 == 0; }
>> [1, 2, 3, 4, 5].filter(isEven)
[2, 4]

array.concat(array other) -> array

Concatenates two arrays.

>> [1, 2].concat([3, 4])
[1, 2, 3, 4]

tuple methods

tuple[uint256 index] -> any

Returns the element at the given index.

tuple.length -> uint256

Returns the length of the tuple.

tuple.map(function f) -> array

Applies the function f to each element of the tuple and returns a new tuple with the results.

address methods

address.balance -> uint256

Returns the balance of the address.

num (uint* and int*) methods

num.mul(num other) -> num | num.mul(num other, uint8 decimals) -> num

Multiplies two scaled numbers. By default, the number are assumed to have 18 decimals. The second argument can be used to specify the number of decimals.

>> 2e18.mul(3e18)
6000000000000000000

num.div(num other) -> num | num.div(num other, uint8 decimals) -> num

Divides two scaled numbers using similar logic to mul.

Transaction methods

Transaction.getReceipt() -> Receipt | Transaction.getReceipt(uint256 timeout) -> Receipt

Returns the receipt of the transaction. The function will wait for the transaction for up to timeout seconds, or 30 seconds by default.

>> tx = Transaction(0xfb89e2333b81f2751eedaf2aeffb787917d42ea6ea7c5afd4d45371f3f1b8079)
>> tx.getReceipt()
Receipt { txHash: 0xfb89e2333b81f2751eedaf2aeffb787917d42ea6ea7c5afd4d45371f3f1b8079, blockHash: 0xd82cbdd9aba2827815d8db2e0665b1f54e6decc4f59042e53344f6562301e55b, blockNumber: 18735365, status: true, gasUsed: 54017, gasPrice: 71885095452, logs: [Log { address: 0xe07F9D810a48ab5c3c914BA3cA53AF14E4491e8A, topics: [0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef, 0x00000000000000000000000035641673a0ce64f644ad2f395be19668a06a5616, 0x0000000000000000000000009748a9de5a2d81e96c2070f7f0d1d128bbb4d3c4], data: 0x00000000000000000000000000000000000000000000007b1638669932a6793d, args: Transfer { from: 0x35641673A0Ce64F644Ad2f395be19668A06A5616, to: 0x9748a9dE5A2D81e96C2070f7F0D1d128BbB4d3c4, value: 2270550663541970860349 } }] }

The receipt is a named tuple with the following fields:

  • txHash (bytes32): the hash of the transaction.
  • blockHash (bytes32): the hash of the block containing the transaction.
  • blockNumber (uint256): the number of the block containing the transaction.
  • status (bool): the status of the transaction.
  • gasUsed (uint256): the amount of gas used by the transaction.
  • gasPrice (uint256): the gas price of the transaction.
  • logs (NamedTuple[]): the logs of the transaction.

Logs is an array of named tuples with the following fields:

  • address (address): the address of the contract that emitted the log.
  • topics (bytes32[]): the topics of the log.
  • data (bytes): the data of the log.
  • args (NamedTuple): the decoded arguments of the log, if the event is known (present in one of the loaded ABIs).

Contract static methods

These methods are available on the contracts themselves, not on their instances.

Contract.decode(bytes data) -> (string, tuple)

Decodes the ABI-encoded data into a tuple. The first element of the tuple is the function signature, and the second element is the decoded arguments.

>> ERC20.decode(0xa9059cbb000000000000000000000000789f8f7b547183ab8e99a5e0e6d567e90e0eb03b0000000000000000000000000000000000000000000000056bc75e2d63100000)
("transfer(address,uint256)", (0x789f8F7B547183Ab8E99A5e0E6D567E90e0EB03B, 100000000000000000000))

Event static methods

Event.selector -> bytes32

Returns the selector (aka topic0) of the given event

num (uint* and int*) static methods

type(num).max -> num

Returns the maximum value of the type.

>> type(uint8).max
255

type(num).min -> num

Returns the minimum value of the type.

>> type(int8).min
-128