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 resulttraceCall
: 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 resultencode
: 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 themsg.value
of the transactionblock
: sets the block number to execute the call on (only works for calls, not for sending transactions)from
: sets thefrom
for the call (only works for calls, not for sending transactions)gasLimit
: sets the gas limit to use for the transactionmaxFee
: sets the maximum fee to pay for the transactionpriorityFee
: sets the priority fee to pay for the transactiongasPrice
: 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 fromtoBlock
: the block number to stop fetching events attopic0
: topic0 of the eventtopic1
: topic1 of the eventtopic2
: topic2 of the eventtopic3
: 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 delegatecallto
: Target addressvalue
: ETH value in weidata
: 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