diff --git a/Cargo.lock b/Cargo.lock index 8e455700c75c..57fd99592008 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -437,6 +437,19 @@ dependencies = [ "serde_json", ] +[[package]] +name = "cast" +version = "0.1.0" +dependencies = [ + "chrono 0.2.25", + "ethers-core", + "ethers-providers", + "eyre", + "foundry-utils", + "rustc-hex", + "serde_json", +] + [[package]] name = "cc" version = "1.0.70" @@ -701,67 +714,6 @@ dependencies = [ "cipher 0.3.0", ] -[[package]] -name = "dapp" -version = "0.1.0" -dependencies = [ - "dapp-utils", - "ethers", - "evm", - "evm-adapters", - "evmodin", - "eyre", - "glob", - "hex", - "proptest", - "regex", - "semver 1.0.4", - "serde", - "serde_json", - "tokio", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "dapp-utils" -version = "0.1.0" -dependencies = [ - "ethers-core", - "eyre", - "rustc-hex", - "serde_json", -] - -[[package]] -name = "dapptools" -version = "0.1.0" -dependencies = [ - "ansi_term 0.12.1", - "dapp", - "dapp-utils", - "ethers", - "ethers-etherscan", - "evm", - "evm-adapters", - "evmodin", - "eyre", - "git2", - "glob", - "proptest", - "regex", - "rpassword", - "rustc-hex", - "semver 1.0.4", - "serde_json", - "seth", - "structopt", - "tempdir", - "tokio", - "tracing", - "tracing-subscriber", -] - [[package]] name = "der" version = "0.4.3" @@ -1220,11 +1172,11 @@ name = "evm-adapters" version = "0.1.0" dependencies = [ "bytes", - "dapp-utils", "ethers", "evm", "evmodin", "eyre", + "foundry-utils", "futures", "hex", "once_cell", @@ -1357,6 +1309,28 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "forge" +version = "0.1.0" +dependencies = [ + "ethers", + "evm", + "evm-adapters", + "evmodin", + "eyre", + "foundry-utils", + "glob", + "hex", + "proptest", + "regex", + "semver 1.0.4", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "form_urlencoded" version = "1.0.1" @@ -1367,6 +1341,45 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "foundry-cli" +version = "0.1.0" +dependencies = [ + "ansi_term 0.12.1", + "cast", + "ethers", + "ethers-etherscan", + "evm", + "evm-adapters", + "evmodin", + "eyre", + "forge", + "foundry-utils", + "git2", + "glob", + "proptest", + "regex", + "rpassword", + "rustc-hex", + "semver 1.0.4", + "serde_json", + "structopt", + "tempdir", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "foundry-utils" +version = "0.1.0" +dependencies = [ + "ethers-core", + "eyre", + "rustc-hex", + "serde_json", +] + [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -3284,19 +3297,6 @@ dependencies = [ "serde", ] -[[package]] -name = "seth" -version = "0.1.0" -dependencies = [ - "chrono 0.2.25", - "dapp-utils", - "ethers-core", - "ethers-providers", - "eyre", - "rustc-hex", - "serde_json", -] - [[package]] name = "sha2" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index 87aad1538536..e81c8edfc97a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,9 +2,9 @@ members = [ "evm-adapters", "utils", - "seth", - "dapp", - "dapptools", + "cast", + "forge", + "cli", ] # Binary size optimizations diff --git a/README.md b/README.md index f9a1642b3fe6..6003b70f04f7 100644 --- a/README.md +++ b/README.md @@ -1,176 +1,108 @@ -#

dapptools.rs

+#

foundry

-_Rust port of DappTools_ - -![Github Actions](https://github.com/gakonst/dapptools-rs/workflows/Tests/badge.svg) -[![Telegram Chat](https://img.shields.io/endpoint?color=neon&style=flat-square&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fturbodapptools)](https://t.me/turbodapptools) +![Github Actions](https://github.com/gakonst/foundry/workflows/Tests/badge.svg) +[![Telegram Chat](https://img.shields.io/endpoint?color=neon&style=flat-square&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Ffoundry_rs)](https://t.me/foundry_rs) [![Crates.io][crates-badge]][crates-url] -[crates-badge]: https://img.shields.io/crates/v/turbodapp.svg -[crates-url]: https://crates.io/crates/turbodapp +[crates-badge]: https://img.shields.io/crates/v/foundry.svg +[crates-url]: https://crates.io/crates/foundry-rs + +**Foundry is a blazing fast, portable and modular toolkit for Ethereum +application development written in Rust.** + +Foundry consists of: + +- [**Forge**](./forge): Ethereum testing framework (like Truffle, Hardhat and + Dapptools). +- [**Cast**](./cast): Swiss army knife for interacting with EVM smart contracts, + sending transactions and getting chain data. -## Installing +![demo](./assets/demo.svg) -We have not published a release yet. Until we do, please use the command below. -Because our dependencies may not be stable, do not forget the `--locked` -parameter, which will force the installer to use the checked in `Cargo.lock` -file. +## Forge Quickstart + +Forge is a development and testing framework for Ethereum applications. ``` -cargo install --git https://github.com/gakonst/dapptools-rs --locked +cargo install forge +forge init +forge build +forge test ``` -Alternatively, clone the repository and run: `cargo build --release` - -## Why?! DappTools is great! - -Developer experience is the #1 thing we should be optimizing for in development. -Tests MUST be fast, non-trivial tests (e.g. proptests) MUST be easy to write, -and compilation MUST be fast. - -Before getting into technical reasons, my simple answer is: rewriting software -in Rust is fun. I enjoy it, and that could be the end of the "why" section. - -DappTools is REALLY great. -[You should try it](https://github.com/dapphub/dapptools/), especially the -symbolic execution and step debugger features. - -But it has some shortcomings: - -It's written in a mix of Bash, Javascript and Haskell. In my opinion, this makes -it hard to contribute, you don't have a "standard" way to test things, and it -happens to be that there are not that many Haskell developers in the Ethereum -community. - -It is also hard to distribute. It requires installing Nix, and that's a barrier -to entry to many already because (for whatever reason) Nix doesn't always -install properly the first time. - -The more technical reasons I decided to use it are: - -1. It is easier to write regression tests in Rust than in Bash. -1. Rust binaries are cross-platform and easy to distribute. -1. Compilation speed: We can use native bindings to the Solidity compiler - (instead of calling out to solcjs or even to the compiled binary) for extra - compilation speed. -1. Testing speed: HEVM tests are really fast, but I believe we can go faster by - leveraging Rust's high performance multithreading and resource allocation - system. -1. There seems to be an emerging community of Rust-Ethereum developers. - -Benchmarks TBD in the future, but: - -1. [Using a Rust EVM w/ forked RPC mode](https://github.com/brockelmore/rust-cevm/#compevm-rust-ethereum-virtual-machine-implementation-designed-for-smart-contract-composability-testing) - was claimed to be as high as 10x faster than HEVM's forking mode. -1. Native bindings to the Solidity compiler have been shown to be - [10x](https://forum.openzeppelin.com/t/a-faster-solidity-compiler-cli-in-rust/2546) - faster than the JS bindings or even just calling out to the native binary. -1. `seth` and `dapp` are less than 7mb when built with `cargo build --release`. - -## Features - -- seth - - [ ] `--abi-decode` - - [ ] `--calldata-decode` - - [x] `--from-ascii` (with `--from-utf8` alias) - - [ ] `--from-bin` - - [ ] `--from-fix` - - [ ] `--from-wei` - - [ ] `--max-int` - - [ ] `--max-uint` - - [ ] `--min-int` - - [x] `--to-checksum-address` (`--to-address` in dapptools) - - [x] `--to-ascii` - - [x] `--to-bytes32` - - [x] `--to-dec` - - [x] `--to-fix` - - [x] `--to-hex` - - [x] `--to-hexdata` - - [ ] `--to-int256` - - [x] `--to-uint256` - - [x] `--to-wei` - - [ ] `4byte` - - [ ] `4byte-decode` - - [ ] `4byte-event` - - [ ] `abi-encode` - - [x] `age` - - [x] `balance` - - [x] `basefee` - - [x] `block` - - [x] `block-number` - - [ ] `bundle-source` - - [x] `call` (partial) - - [x] `calldata` - - [x] `chain` - - [x] `chain-id` - - [ ] `code` - - [ ] `debug` - - [ ] `estimate` - - [ ] `etherscan-source` - - [ ] `events` - - [x] `gas-price` - - [ ] `index` - - [x] `keccak` - - [ ] `logs` - - [x] `lookup-address` - - [ ] `ls` - - [ ] `mktx` - - [x] `namehash` - - [ ] `nonce` - - [ ] `publish` - - [ ] `receipt` - - [x] `resolve-name` - - [ ] `run-tx` - - [x] `send` (partial) - - [ ] `sign` - - [x] `storage` - - [ ] `tx` -- dapp - - [ ] test - - [x] Simple unit tests - - [x] Gas costs - - [x] DappTools style test output - - [x] JSON test output - - [x] Matching on regex - - [x] DSTest-style assertions support - - [x] Fuzzing - - [ ] Symbolic execution - - [ ] Coverage - - [ ] HEVM-style Solidity cheatcodes - - [x] roll: Sets block.number - - [x] warp: Sets block.timestamp - - [x] ffi: Perform foreign function call to terminal - - [x] store: Sets address storage slot - - [x] load: Loads address storage slot - - [x] deal: Sets account balance - - [x] prank: Performs a call as another address (changes msg.sender for a call) - - [x] sign: Signs data - - [x] addr: Gets address for a private key - - [ ] makeEOA - - ...? - - [ ] Structured tracing with abi decoding - - [ ] Per-line gas profiling - - [x] Forking mode - - [x] Automatic solc selection - - [x] build - - [x] Can read DappTools-style .sol.json artifacts - - [x] Manual remappings - - [x] Automatic remappings - - [x] Multiple compiler versions - - [ ] Incremental compilation - - [ ] Can read Hardhat-style artifacts - - [ ] Can read Truffle-style artifacts - - [x] install - - [x] update - - [ ] debug - - [x] CLI Tracing with `RUST_LOG=dapp=trace` - -## Tested Against - -This repository has been tested against a few repositories which you can monitor -[here](https://github.com/gakonst/dapptools-benchmarks) - -## Development +More documentation can be found in the [forge package](./forge/README.md). + +### Features + +1. Fast & flexible compilation pipeline: + 1. Automatic Solidity compiler version detection & installation (under + `~/.svm`) + 1. Incremental compilation & caching: Only changed files are re-compiled + 1. Parallel compilation + 1. Non-standard directory structures support (e.g. can build + [Hardhat repos](https://twitter.com/gakonst/status/1461289225337421829)) +1. Tests are written in Solidity (like in DappTools) +1. Fast fuzz Tests with shrinking of inputs & printing of counter-examples +1. Fast remote RPC forking mode leveraging Rust's async infrastructure like + tokio +1. Flexible debug logging: + 1. Dapptools-style, using `DsTest`'s emitted logs + 1. Hardhat-style, using the popular `console.sol` contract +1. Portable (5-10MB) & easy to install statically linked binary without + requiring Nix or any other package manager +1. Abstracted over EVM implementations (currently supported: Sputnik, EvmOdin) + +### How Fast? + +Forge is quite fast at both compiling (leveraging the +[ethers-solc](https://github.com/gakonst/ethers-rs/tree/master/ethers-solc/) +package) and testing. + +Some benchmarks below: + +| Project | Forge | DappTools | Speedup | +| --------------------------------------------------- | ----- | --------- | ------- | +| [guni-lev](https://github.com/hexonaut/guni-lev/) | 28.6s | 2m36s | 5.45x | +| [solmate](https://github.com/Rari-Capital/solmate/) | 6s | 46s | 7.66x | +| [geb](https://github.com/reflexer-labs/geb) | 11s | 40s | 3.63x | +| [vaults](https://github.com/rari-capital/vaults) | 1.4s | 5.5s | 3.9x | + +It also works with "non-standard" directory structures (i.e. contracts not in +`src/`, libraries not in `lib/`). When +[tested](https://twitter.com/gakonst/status/1461289225337421829) with +[`openzeppelin-contracts`](https://github.com/OpenZeppelin/openzeppelin-contracts), +Hardhat compilation took 15.244s, whereas Forge took 9.449 (~4s cached) + +## Cast Quickstart + +Cast is a swiss army knife for interacting with Ethereum applications from the +command line. + +``` +cargo install cast +cast call 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 "totalSupply()" --rpc-url $ETH_RPC_URL +``` + +More documentation can be found in the [cast package](./cast/README.md). + +## Contributing + +### Directory structure + +This repository contains several Rust crates: + +- [`forge`](forge): Library for building and testing a Solidity repository. +- [`cast`](cast): Library for interacting with a live Ethereum JSON-RPC + compatible node, or for parsing data. +- [`cli`](cli): Command line interfaces to `cast` and `forge`. +- [`evm-adapters`](evm-adapters): Unified layer of abstraction over multiple EVM + types. Currently supported EVMs: + [Sputnik](https://github.com/rust-blockchain/evm/), + [Evmodin](https://github.com/vorot93/evmodin). +- [`utils`](utils): Utilities for parsing ABI data, will eventually be + upstreamed to [ethers-rs](https://github.com/gakonst/ethers-rs/). + +The minimum supported rust version is 1.51. ### Rust Toolchain @@ -201,7 +133,23 @@ cargo clippy First, see if the answer to your question can be found in the API documentation. If the answer is not there, try opening an -[issue](https://github.com/gakonst/dapptools-rs/issues/new) with the question. - -Join the [turbodapptools telegram](https://t.me/turbodapptools) to chat with the -community! +[issue](https://github.com/gakonst/foundry/issues/new) with the question. + +Join the [foundry telegram](https://t.me/foundry_rs) to chat with the community! + +## Acknowledgements + +- Foundry is a clean-room rewrite of the testing framework + [dapptools](https://github.com/dapphub/dapptools). None of this would have + been possible without the DappHub team's work over the years. +- [Matthias Seitz](https://twitter.com/mattsse_): Created + [ethers-solc](https://github.com/gakonst/ethers-rs/tree/master/ethers-solc/) + which is the backbone of our compilation pipeline, as well as countless + contributions to ethers, in particular the `abigen` macros. +- [Rohit Narunkar](https://twitter.com/rohitnarurkar): Created the Rust Solidity + version manager [svm-rs](https://github.com/roynalnaruto/svm-rs) which we use + to auto-detect and manage multiple Solidity versions. +- All the other + [contributors](https://github.com/gakonst/foundry/graphs/contributors) to the + [ethers-rs](https://github.com/gakonst/ethers-rs) & + [foundry](https://github.com/gakonst/foundry) repositories and chatrooms. diff --git a/assets/demo.svg b/assets/demo.svg new file mode 100644 index 000000000000..08fe56a6a1d1 --- /dev/null +++ b/assets/demo.svg @@ -0,0 +1 @@ +..tdata/solmatesolmategit:(07af469)solmategit:(07af469)forgetestforgesuccess.Running6testsforFixedPointMathLibTest[PASS]testMin(gas:[fuzztest])[PASS]testFPow(gas:1822)[PASS]testMax(gas:[fuzztest])[PASS]testSqrt(gas:[fuzztest])[PASS]testFDiv(gas:1733)[PASS]testFMul(gas:1755)Running2testsforTrustAuthorityTest[PASS]testSanityChecks(gas:8632)[PASS]testUpdateTrust(gas:24421)Running3testsforReentrancyGuardTest[PASS]testNoReentrancy(gas:1204)[PASS]testProtectedCall(gas:23995)[PASS]testFailUnprotectedCall(gas:32014)Running4testsforRolesAuthorityTest[PASS]testPublicCapabilities(gas:55328)[PASS]testBasics(gas:94619)[PASS]testSanityChecks(gas:20830)[PASS]testRoot(gas:52297)Running5testsforAuthTest[PASS]testFailRejectingAuthority1(gas:166986)[PASS]testAcceptingOwner(gas:206772)[PASS]testFailRejectingAuthority2(gas:167429)[PASS]testFailNonOwner1(gas:6662)[PASS]testFailNonOwner2(gas:7106)Running2testsforBytes32AddressLibTest[PASS]testFromLast20Bytes(gas:310)[PASS]testFillLast12Bytes(gas:351)Running1testforERC20Test[PASS]testMetaData(gas:[fuzztest])solmategit:(07af469)fsolmategit:(07af469)forgetestforge[PASS]testFailRejectingAuthority2(gas: \ No newline at end of file diff --git a/seth/Cargo.toml b/cast/Cargo.toml similarity index 89% rename from seth/Cargo.toml rename to cast/Cargo.toml index cca0aa8cdf50..a4a37482d5e3 100644 --- a/seth/Cargo.toml +++ b/cast/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "seth" +name = "cast" version = "0.1.0" edition = "2018" license = "MIT OR Apache-2.0" @@ -7,7 +7,7 @@ license = "MIT OR Apache-2.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dapp-utils = { path = "../utils" } +foundry-utils = { path = "../utils" } # ethers = "0.5" ethers-core = { git = "https://github.com/gakonst/ethers-rs", default-features = false } ethers-providers = { git = "https://github.com/gakonst/ethers-rs", default-features = false } diff --git a/cast/README.md b/cast/README.md new file mode 100644 index 000000000000..8e8397c0b5f4 --- /dev/null +++ b/cast/README.md @@ -0,0 +1,59 @@ +# `cast` + +## Features + +- [ ] `--abi-decode` +- [ ] `--calldata-decode` +- [x] `--from-ascii` (with `--from-utf8` alias) +- [ ] `--from-bin` +- [ ] `--from-fix` +- [ ] `--from-wei` +- [ ] `--max-int` +- [ ] `--max-uint` +- [ ] `--min-int` +- [x] `--to-checksum-address` (`--to-address` in dapptools) +- [x] `--to-ascii` +- [x] `--to-bytes32` +- [x] `--to-dec` +- [x] `--to-fix` +- [x] `--to-hex` +- [x] `--to-hexdata` +- [ ] `--to-int256` +- [x] `--to-uint256` +- [x] `--to-wei` +- [ ] `4byte` +- [ ] `4byte-decode` +- [ ] `4byte-event` +- [ ] `abi-encode` +- [x] `age` +- [x] `balance` +- [x] `basefee` +- [x] `block` +- [x] `block-number` +- [ ] `bundle-source` +- [x] `call` (partial) +- [x] `calldata` +- [x] `chain` +- [x] `chain-id` +- [ ] `code` +- [ ] `debug` +- [ ] `estimate` +- [ ] `etherscan-source` +- [ ] `events` +- [x] `gas-price` +- [ ] `index` +- [x] `keccak` +- [ ] `logs` +- [x] `lookup-address` +- [ ] `ls` +- [ ] `mktx` +- [x] `namehash` +- [ ] `nonce` +- [ ] `publish` +- [ ] `receipt` +- [x] `resolve-name` +- [ ] `run-tx` +- [x] `send` (partial) +- [ ] `sign` +- [x] `storage` +- [ ] `tx` diff --git a/seth/src/lib.rs b/cast/src/lib.rs similarity index 82% rename from seth/src/lib.rs rename to cast/src/lib.rs index 8c5b69b5172f..43e9c423987f 100644 --- a/seth/src/lib.rs +++ b/cast/src/lib.rs @@ -1,4 +1,4 @@ -//! Seth +//! Cast //! //! TODO use chrono::NaiveDateTime; @@ -12,28 +12,28 @@ use eyre::Result; use rustc_hex::{FromHexIter, ToHex}; use std::str::FromStr; -use dapp_utils::{encode_args, get_func, to_table}; +use foundry_utils::{encode_args, get_func, to_table}; -// TODO: SethContract with common contract initializers? Same for SethProviders? +// TODO: CastContract with common contract initializers? Same for CastProviders? -pub struct Seth { +pub struct Cast { provider: M, } -impl Seth +impl Cast where M::Error: 'static, { /// Converts ASCII text input to hex /// /// ``` - /// use seth::Seth; + /// use cast::Cast; /// use ethers_providers::{Provider, Http}; /// use std::convert::TryFrom; /// /// # async fn foo() -> eyre::Result<()> { /// let provider = Provider::::try_from("http://localhost:8545")?; - /// let seth = Seth::new(provider); + /// let cast = Cast::new(provider); /// # Ok(()) /// # } /// ``` @@ -45,18 +45,18 @@ where /// /// ```no_run /// - /// use seth::Seth; + /// use cast::Cast; /// use ethers_core::types::Address; /// use ethers_providers::{Provider, Http}; /// use std::{str::FromStr, convert::TryFrom}; /// /// # async fn foo() -> eyre::Result<()> { /// let provider = Provider::::try_from("http://localhost:8545")?; - /// let seth = Seth::new(provider); + /// let cast = Cast::new(provider); /// let to = Address::from_str("0xB3C95ff08316fb2F2e3E52Ee82F8e7b605Aa1304")?; /// let sig = "function greeting(uint256 i) public returns (string)"; /// let args = vec!["5".to_owned()]; - /// let data = seth.call(to, sig, args).await?; + /// let data = cast.call(to, sig, args).await?; /// println!("{}", data); /// # Ok(()) /// # } @@ -98,18 +98,18 @@ where /// Sends a transaction to the specified address /// /// ```no_run - /// use seth::Seth; + /// use cast::Cast; /// use ethers_core::types::Address; /// use ethers_providers::{Provider, Http}; /// use std::{str::FromStr, convert::TryFrom}; /// /// # async fn foo() -> eyre::Result<()> { /// let provider = Provider::::try_from("http://localhost:8545")?; - /// let seth = Seth::new(provider); + /// let cast = Cast::new(provider); /// let to = Address::from_str("0xB3C95ff08316fb2F2e3E52Ee82F8e7b605Aa1304")?; /// let sig = "function greet(string memory) public returns (string)"; /// let args = vec!["5".to_owned()]; - /// let data = seth.call(to, sig, args).await?; + /// let data = cast.call(to, sig, args).await?; /// println!("{}", data); /// # Ok(()) /// # } @@ -140,14 +140,14 @@ where } /// ```no_run - /// use seth::Seth; + /// use cast::Cast; /// use ethers_providers::{Provider, Http}; /// use std::convert::TryFrom; /// /// # async fn foo() -> eyre::Result<()> { /// let provider = Provider::::try_from("http://localhost:8545")?; - /// let seth = Seth::new(provider); - /// let block = seth.block(5, true, None, false).await?; + /// let cast = Cast::new(provider); + /// let block = cast.block(5, true, None, false).await?; /// println!("{}", block); /// # Ok(()) /// # } @@ -199,7 +199,7 @@ where async fn block_field_as_num>(&self, block: T, field: String) -> Result { let block = block.into(); - let block_field = Seth::block( + let block_field = Cast::block( self, block, false, @@ -213,18 +213,18 @@ where } pub async fn base_fee>(&self, block: T) -> Result { - Ok(Seth::block_field_as_num(self, block, String::from("baseFeePerGas")).await?) + Ok(Cast::block_field_as_num(self, block, String::from("baseFeePerGas")).await?) } pub async fn age>(&self, block: T) -> Result { let timestamp_str = - Seth::block_field_as_num(self, block, String::from("timestamp")).await?.to_string(); + Cast::block_field_as_num(self, block, String::from("timestamp")).await?.to_string(); let datetime = NaiveDateTime::from_timestamp(timestamp_str.parse::().unwrap(), 0); Ok(datetime.format("%a %b %e %H:%M:%S %Y").to_string()) } pub async fn chain(&self) -> Result<&str> { - let genesis_hash = Seth::block( + let genesis_hash = Cast::block( self, 0, false, @@ -236,7 +236,7 @@ where Ok(match &genesis_hash[..] { "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" => { - match &(Seth::block(self, 1920000, false, Some(String::from("hash")), false) + match &(Cast::block(self, 1920000, false, Some(String::from("hash")), false) .await?)[..] { "0x94365e3a8c0b35089c1d1195081fe7489b528a84b22199c916180db8b28ade7f" => { @@ -279,14 +279,14 @@ where } } -pub struct SimpleSeth; -impl SimpleSeth { +pub struct SimpleCast; +impl SimpleCast { /// Converts UTF-8 text input to hex /// /// ``` - /// use seth::SimpleSeth as Seth; + /// use cast::SimpleCast as Cast; /// - /// let bin = Seth::from_utf8("yo"); + /// let bin = Cast::from_utf8("yo"); /// assert_eq!(bin, "0x796f") /// ``` pub fn from_utf8(s: &str) -> String { @@ -297,11 +297,11 @@ impl SimpleSeth { /// Converts hex data into text data /// /// ``` - /// use seth::SimpleSeth as Seth; + /// use cast::SimpleCast as Cast; /// /// fn main() -> eyre::Result<()> { - /// assert_eq!("Hello, World!", Seth::ascii("48656c6c6f2c20576f726c6421")?); - /// assert_eq!("TurboDappTools", Seth::ascii("0x547572626f44617070546f6f6c73")?); + /// assert_eq!("Hello, World!", Cast::ascii("48656c6c6f2c20576f726c6421")?); + /// assert_eq!("TurboDappTools", Cast::ascii("0x547572626f44617070546f6f6c73")?); /// /// Ok(()) /// } @@ -319,12 +319,12 @@ impl SimpleSeth { /// Converts hex input to decimal /// /// ``` - /// use seth::SimpleSeth as Seth; + /// use cast::SimpleCast as Cast; /// use ethers_core::types::U256; /// /// fn main() -> eyre::Result<()> { - /// assert_eq!(U256::from_dec_str("424242")?, Seth::to_dec("0x67932")?); - /// assert_eq!(U256::from_dec_str("1234")?, Seth::to_dec("0x4d2")?); + /// assert_eq!(U256::from_dec_str("424242")?, Cast::to_dec("0x67932")?); + /// assert_eq!(U256::from_dec_str("1234")?, Cast::to_dec("0x4d2")?); /// /// Ok(()) /// } @@ -335,14 +335,14 @@ impl SimpleSeth { /// Converts integers with specified decimals into fixed point numbers /// /// ``` - /// use seth::SimpleSeth as Seth; + /// use cast::SimpleCast as Cast; /// use ethers_core::types::U256; /// /// fn main() -> eyre::Result<()> { - /// assert_eq!(Seth::to_fix(0, 10.into())?, "10."); - /// assert_eq!(Seth::to_fix(1, 10.into())?, "1.0"); - /// assert_eq!(Seth::to_fix(2, 10.into())?, "0.10"); - /// assert_eq!(Seth::to_fix(3, 10.into())?, "0.010"); + /// assert_eq!(Cast::to_fix(0, 10.into())?, "10."); + /// assert_eq!(Cast::to_fix(1, 10.into())?, "1.0"); + /// assert_eq!(Cast::to_fix(2, 10.into())?, "0.10"); + /// assert_eq!(Cast::to_fix(3, 10.into())?, "0.010"); /// /// Ok(()) /// } @@ -364,13 +364,13 @@ impl SimpleSeth { /// Converts decimal input to hex /// /// ``` - /// use seth::SimpleSeth as Seth; + /// use cast::SimpleCast as Cast; /// use ethers_core::types::U256; /// /// fn main() -> eyre::Result<()> { - /// assert_eq!(Seth::hex(U256::from_dec_str("424242")?), "0x67932"); - /// assert_eq!(Seth::hex(U256::from_dec_str("1234")?), "0x4d2"); - /// assert_eq!(Seth::hex(U256::from_dec_str("115792089237316195423570985008687907853269984665640564039457584007913129639935")?), "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + /// assert_eq!(Cast::hex(U256::from_dec_str("424242")?), "0x67932"); + /// assert_eq!(Cast::hex(U256::from_dec_str("1234")?), "0x4d2"); + /// assert_eq!(Cast::hex(U256::from_dec_str("115792089237316195423570985008687907853269984665640564039457584007913129639935")?), "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); /// /// Ok(()) /// } @@ -382,13 +382,13 @@ impl SimpleSeth { /// Converts a number into uint256 hex string with 0x prefix /// /// ``` - /// use seth::SimpleSeth as Seth; + /// use cast::SimpleCast as Cast; /// /// fn main() -> eyre::Result<()> { - /// assert_eq!(Seth::to_uint256("100")?, "0x0000000000000000000000000000000000000000000000000000000000000064"); - /// assert_eq!(Seth::to_uint256("192038293923")?, "0x0000000000000000000000000000000000000000000000000000002cb65fd1a3"); + /// assert_eq!(Cast::to_uint256("100")?, "0x0000000000000000000000000000000000000000000000000000000000000064"); + /// assert_eq!(Cast::to_uint256("192038293923")?, "0x0000000000000000000000000000000000000000000000000000002cb65fd1a3"); /// assert_eq!( - /// Seth::to_uint256("115792089237316195423570985008687907853269984665640564039457584007913129639935")?, + /// Cast::to_uint256("115792089237316195423570985008687907853269984665640564039457584007913129639935")?, /// "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" /// ); /// @@ -404,13 +404,13 @@ impl SimpleSeth { /// Converts an eth amount into wei /// /// ``` - /// use seth::SimpleSeth as Seth; + /// use cast::SimpleCast as Cast; /// /// fn main() -> eyre::Result<()> { - /// assert_eq!(Seth::to_wei(1.into(), "".to_string())?, "1"); - /// assert_eq!(Seth::to_wei(100.into(), "gwei".to_string())?, "100000000000"); - /// assert_eq!(Seth::to_wei(100.into(), "eth".to_string())?, "100000000000000000000"); - /// assert_eq!(Seth::to_wei(1000.into(), "ether".to_string())?, "1000000000000000000000"); + /// assert_eq!(Cast::to_wei(1.into(), "".to_string())?, "1"); + /// assert_eq!(Cast::to_wei(100.into(), "gwei".to_string())?, "100000000000"); + /// assert_eq!(Cast::to_wei(100.into(), "eth".to_string())?, "100000000000000000000"); + /// assert_eq!(Cast::to_wei(1000.into(), "ether".to_string())?, "1000000000000000000000"); /// /// Ok(()) /// } @@ -428,13 +428,13 @@ impl SimpleSeth { /// according to [EIP-55](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md) /// /// ``` - /// use seth::SimpleSeth as Seth; + /// use cast::SimpleCast as Cast; /// use ethers_core::types::Address; /// use std::str::FromStr; /// /// # fn main() -> eyre::Result<()> { /// let addr = Address::from_str("0xb7e390864a90b7b923c9f9310c6f98aafe43f707")?; - /// let addr = Seth::checksum_address(&addr)?; + /// let addr = Cast::checksum_address(&addr)?; /// assert_eq!(addr, "0xB7e390864a90b7b923C9f9310C6F98aafE43F707"); /// /// # Ok(()) @@ -446,16 +446,16 @@ impl SimpleSeth { /// Converts hexdata into bytes32 value /// ``` - /// use seth::SimpleSeth as Seth; + /// use cast::SimpleCast as Cast; /// /// # fn main() -> eyre::Result<()> { - /// let bytes = Seth::bytes32("1234")?; + /// let bytes = Cast::bytes32("1234")?; /// assert_eq!(bytes, "0x1234000000000000000000000000000000000000000000000000000000000000"); /// - /// let bytes = Seth::bytes32("0x1234")?; + /// let bytes = Cast::bytes32("0x1234")?; /// assert_eq!(bytes, "0x1234000000000000000000000000000000000000000000000000000000000000"); /// - /// let err = Seth::bytes32("0x123400000000000000000000000000000000000000000000000000000000000011").unwrap_err(); + /// let err = Cast::bytes32("0x123400000000000000000000000000000000000000000000000000000000000011").unwrap_err(); /// assert_eq!(err.to_string(), "string >32 bytes"); /// /// # Ok(()) @@ -474,11 +474,11 @@ impl SimpleSeth { /// Keccak-256 hashes arbitrary data /// /// ``` - /// use seth::SimpleSeth as Seth; + /// use cast::SimpleCast as Cast; /// /// fn main() -> eyre::Result<()> { - /// assert_eq!(Seth::keccak("foo")?, "0x41b1a0649752af1b28b3dc29a1556eee781e4a4c3a1f7f53f90fa834de098c4d"); - /// assert_eq!(Seth::keccak("123abc")?, "0xb1f1c74a1ba56f07a892ea1110a39349d40f66ca01d245e704621033cb7046a4"); + /// assert_eq!(Cast::keccak("foo")?, "0x41b1a0649752af1b28b3dc29a1556eee781e4a4c3a1f7f53f90fa834de098c4d"); + /// assert_eq!(Cast::keccak("123abc")?, "0xb1f1c74a1ba56f07a892ea1110a39349d40f66ca01d245e704621033cb7046a4"); /// /// Ok(()) /// } @@ -493,13 +493,13 @@ impl SimpleSeth { /// [namehash-rust reference](https://github.com/InstateDev/namehash-rust/blob/master/src/lib.rs) /// /// ``` - /// use seth::SimpleSeth as Seth; + /// use cast::SimpleCast as Cast; /// /// fn main() -> eyre::Result<()> { - /// assert_eq!(Seth::namehash("")?, "0x0000000000000000000000000000000000000000000000000000000000000000"); - /// assert_eq!(Seth::namehash("eth")?, "0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae"); - /// assert_eq!(Seth::namehash("foo.eth")?, "0xde9b09fd7c5f901e23a3f19fecc54828e9c848539801e86591bd9801b019f84f"); - /// assert_eq!(Seth::namehash("sub.foo.eth")?, "0x500d86f9e663479e5aaa6e99276e55fc139c597211ee47d17e1e92da16a83402"); + /// assert_eq!(Cast::namehash("")?, "0x0000000000000000000000000000000000000000000000000000000000000000"); + /// assert_eq!(Cast::namehash("eth")?, "0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae"); + /// assert_eq!(Cast::namehash("foo.eth")?, "0xde9b09fd7c5f901e23a3f19fecc54828e9c848539801e86591bd9801b019f84f"); + /// assert_eq!(Cast::namehash("sub.foo.eth")?, "0x500d86f9e663479e5aaa6e99276e55fc139c597211ee47d17e1e92da16a83402"); /// /// Ok(()) /// } @@ -528,19 +528,19 @@ impl SimpleSeth { /// Performs ABI encoding to produce the hexadecimal calldata with the given arguments. /// /// ``` - /// # use seth::SimpleSeth as Seth; + /// # use cast::SimpleCast as Cast; /// /// # fn main() -> eyre::Result<()> { /// assert_eq!( /// "0xb3de648b0000000000000000000000000000000000000000000000000000000000000001", - /// Seth::calldata("f(uint a)", &["1"]).unwrap().as_str() + /// Cast::calldata("f(uint a)", &["1"]).unwrap().as_str() /// ); /// # Ok(()) /// # } /// ``` pub fn calldata(sig: impl AsRef, args: &[impl AsRef]) -> Result { let func = AbiParser::default().parse_function(sig.as_ref())?; - let calldata = dapp_utils::encode_args(&func, args)?; + let calldata = encode_args(&func, args)?; Ok(format!("0x{}", calldata.to_hex::())) } } @@ -551,13 +551,13 @@ fn strip_0x(s: &str) -> &str { #[cfg(test)] mod tests { - use super::SimpleSeth as Seth; + use super::SimpleCast as Cast; #[test] fn calldata_uint() { assert_eq!( "0xb3de648b0000000000000000000000000000000000000000000000000000000000000001", - Seth::calldata("f(uint a)", &["1"]).unwrap().as_str() + Cast::calldata("f(uint a)", &["1"]).unwrap().as_str() ); } @@ -565,7 +565,7 @@ mod tests { fn calldata_bool() { assert_eq!( "0x6fae94120000000000000000000000000000000000000000000000000000000000000000", - Seth::calldata("bar(bool)", &["false"]).unwrap().as_str() + Cast::calldata("bar(bool)", &["false"]).unwrap().as_str() ); } } diff --git a/dapptools/Cargo.toml b/cli/Cargo.toml similarity index 87% rename from dapptools/Cargo.toml rename to cli/Cargo.toml index 0c8cb5715926..55c0223df30a 100644 --- a/dapptools/Cargo.toml +++ b/cli/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "dapptools" +name = "foundry-cli" version = "0.1.0" edition = "2018" license = "MIT OR Apache-2.0" @@ -8,10 +8,11 @@ license = "MIT OR Apache-2.0" [dependencies] structopt = "0.3.23" -dapp-utils = { path = "../utils" } -dapp = { path = "../dapp" } -seth = { path = "../seth" } +foundry-utils = { path = "../utils" } +forge = { path = "../forge" } +cast = { path = "../cast" } evm-adapters = { path = "../evm-adapters" } + # ethers = "0.5" ethers = { git = "https://github.com/gakonst/ethers-rs" } ethers-etherscan = { git = "https://github.com/gakonst/ethers-rs" } @@ -55,11 +56,11 @@ evmodin-evm = [ ] [[bin]] -name = "seth" -path = "src/seth.rs" +name = "cast" +path = "src/cast.rs" doc = false [[bin]] -name = "dapp" -path = "src/dapp.rs" +name = "forge" +path = "src/forge.rs" doc = false diff --git a/cli/README.md b/cli/README.md new file mode 100644 index 000000000000..bff49e8cdab5 --- /dev/null +++ b/cli/README.md @@ -0,0 +1,213 @@ +# Foundry CLIs + +The CLIs are written using [structopt](https://docs.rs/structopt). + +Debug logs are printed with +[`tracing`](https://docs.rs/tracing/0.1.29/tracing/). You can configure the +verbosity level via the +[`RUST_LOG`](https://docs.rs/tracing-subscriber/0.3.2/tracing_subscriber/fmt/index.html#filtering-events-with-environment-variables) +environment variable, on a per package level, +e.g.:`RUST_LOG=forge=trace,evm_adapters=trace forge test` + +## Forge + +``` +foundry-cli 0.1.0 +Build, test, fuzz, formally verify, debug & deploy solidity contracts. + +USAGE: + forge + +FLAGS: + -h, --help Prints help information + -V, --version Prints version information + +SUBCOMMANDS: + build build your smart contracts + create deploy a compiled contract + help Prints this message or the help of the given subcommand(s) + install installs one or more dependencies as git submodules + remappings prints the automatically inferred remappings for this repository + test test your smart contracts + update fetches all upstream lib changes + verify-contract build your smart contracts. Requires `ETHERSCAN_API_KEY` to be set. +``` + +The subcommands are also aliased to their first letter, e.g. you can do +`forge t` instead of `forge test` or `forge b` instead of `forge build`. + +### Build + +The `build` subcommand proceeds to compile your smart contracts. + +``` +forge-build 0.1.0 +build your smart contracts + +USAGE: + forge build [FLAGS] [OPTIONS] + +FLAGS: + -h, --help Prints help information + --no-auto-detect if set to true, skips auto-detecting solc and uses what is in the user's $PATH + -V, --version Prints version information + +OPTIONS: + -c, --contracts the directory relative to the root under which the smart contrats are [env: + DAPP_SRC=] + --evm-version choose the evm version [default: london] + --lib-paths ... the paths where your libraries are installed + -o, --out path to where the contract artifacts are stored + -r, --remappings ... the remappings + --remappings-env [env: DAPP_REMAPPINGS=] + --root the project's root path, default being the current directory [default: + std::env::current_dir().unwrap()] +``` + +By default, it will auto-detect the solc pragma version requirement per-file and +will use the [latest version](https://github.com/ethereum/solidity/releases) +that satisfies the requirement, (e.g. `pragma solidity >=0.7.0 <0.8.0` will use +`solc` 0.7.6). If you want to disable this feature, you can call +`forge build --no-auto-detect`, and it'll use whichever `solc` version is in +your `$PATH`. + +The project's root directory defaults to the current directory, assuming +contracts are under `src/` and `lib/`, but can also be configured via the +`--root`, `--lib-paths` and `--contracts` arguments. The contracts and libraries +directories are assumed to be relative to the project root, for example +`forge build --root ../my-project --contracts my-contracts-dir` will try to find +the contracts under `../my-project/my-contracts-dir`. You can also configure the +output directory where the contract artifacts will be written to with the +`--out` variable. + +Compiler remappings are automatically detected, but if you want to override them +you can do it with the `--remappings` flag like below: + +```bash +$ forge build --remappings @openzeppelin/=node_modules/@openzeppelin/ +``` + +Most of the arguments can also be provided via environment variables, which you +can find by looking for the `env` tooltip in the command's help menu +(`forge build --help`). + +### Test + +Proceeds to build (if needed) and test your smart contracts. It will look for +any contract with a function name that starts with `test`, deploy it, and run +that test function. If that test function takes any arguments, it will proceed +to "fuzz" it (i.e. call it with a lot of different arguments, default: 256 +tries). + +The command re-uses all the options of `forge build`, and also allows you to +configure any blockchain context related variables such as the block coinbase, +difficulty etc. + +``` +forge-test 0.1.0 +test your smart contracts + +USAGE: + forge test [FLAGS] [OPTIONS] + +FLAGS: + --ffi enables the FFI cheatcode + -h, --help Prints help information + -j, --json print the test results in json format + --no-auto-detect if set to true, skips auto-detecting solc and uses what is in the user's $PATH + -V, --version Prints version information + +OPTIONS: + --block-base-fee-per-gas the base fee in a block [default: 0] + --block-coinbase + the block.coinbase value during EVM execution [default: 0x0000000000000000000000000000000000000000] + + --block-difficulty + the block.difficulty value during EVM execution [default: 0] + + --block-gas-limit the block.gaslimit value during EVM execution + --block-number + the block.number value during EVM execution [env: DAPP_TEST_NUMBER=] [default: 0] + + --block-timestamp + the block.timestamp value during EVM execution [env: DAPP_TEST_TIMESTAMP=] [default: 0] + + --chain-id the chainid opcode value [default: 1] + -c, --contracts + the directory relative to the root under which the smart contrats are [env: DAPP_SRC=] + + -e, --evm-type + the EVM type you want to use (e.g. sputnik, evmodin) [default: sputnik] + + --evm-version choose the evm version [default: london] + --fork-block-number + pins the block number for the state fork [env: DAPP_FORK_BLOCK=] + + -f, --fork-url + fetch state over a remote instead of starting from empty state [env: ETH_RPC_URL=] + + --gas-limit the block gas limit [default: 18446744073709551615] + --gas-price the tx.gasprice value during EVM execution [default: 0] + --initial-balance + the initial balance of each deployed test contract [default: 0xffffffffffffffffffffffff] + + --lib-paths ... the paths where your libraries are installed + -o, --out path to where the contract artifacts are stored + -m, --match only run test methods matching regex [default: .*] + -r, --remappings ... the remappings + --remappings-env [env: DAPP_REMAPPINGS=] + --root + the project's root path, default being the current directory [default: std::env::current_dir().unwrap()] + + --sender + the address which will be executing all tests [env: DAPP_TEST_ADDRESS=] [default: + 0x0000000000000000000000000000000000000000] + --tx-origin + the tx.origin value during EVM execution [default: 0x0000000000000000000000000000000000000000] + + --verbosity verbosity of 'forge test' output (0-3) [default: 0] +``` + +Here's how the CLI output looks like when used with +[`dapptools-template`](https://github.com/gakonst/dapptools-template) + +```bash +$ forge test +success. +Running 3 tests for "Greet.json":Greet +[PASS] testCanSetGreeting (gas: 31070) +[PASS] testWorksForAllGreetings (gas: [fuzztest]) +[PASS] testCannotGm (gas: 6819) + +Running 3 tests for "Gm.json":Gm +[PASS] testOwnerCannotGmOnBadBlocks (gas: 7771) +[PASS] testNonOwnerCannotGm (gas: 3782) +[PASS] testOwnerCanGmOnGoodBlocks (gas: 31696) +``` + +You can optionally specify a regular expression, to only run matching functions: + +```bash +$ forge test -m Cannot +$HOME/oss/foundry/target/release/forge test -m Cannot +no files changed, compilation skippped. +Running 1 test for "Greet.json":Greet +[PASS] testCannotGm (gas: 6819) + +Running 2 tests for "Gm.json":Gm +[PASS] testNonOwnerCannotGm (gas: 3782) +[PASS] testOwnerCannotGmOnBadBlocks (gas: 7771) +``` + +In order to compose with other commands, you may print the results as JSON via +the `--json` flag + +```bash +$ forge test --json +no files changed, compilation skippped. +{"\"Gm.json\":Gm":{"testNonOwnerCannotGm":{"success":true,"reason":null,"gas_used":3782,"counterexample":null,"logs":[]},"testOwnerCannotGmOnBadBlocks":{"success":true,"reason":null,"gas_used":7771,"counterexample":null,"logs":[]},"testOwnerCanGmOnGoodBlocks":{"success":true,"reason":null,"gas_used":31696,"counterexample":null,"logs":[]}},"\"Greet.json\":Greet":{"testWorksForAllGreetings":{"success":true,"reason":null,"gas_used":null,"counterexample":null,"logs":[]},"testCannotGm":{"success":true,"reason":null,"gas_used":6819,"counterexample":null,"logs":[]},"testCanSetGreeting":{"success":true,"reason":null,"gas_used":31070,"counterexample":null,"logs":[]}}} +``` + +``` + +``` diff --git a/dapptools/src/seth.rs b/cli/src/cast.rs similarity index 79% rename from dapptools/src/seth.rs rename to cli/src/cast.rs index 9205df927496..7f44b52b830a 100644 --- a/dapptools/src/seth.rs +++ b/cli/src/cast.rs @@ -1,5 +1,7 @@ -mod seth_opts; -use seth_opts::{Opts, Subcommands}; +use cast::{Cast, SimpleCast}; + +mod cast_opts; +use cast_opts::{Opts, Subcommands}; use ethers::{ core::types::{BlockId, BlockNumber::Latest}, @@ -9,7 +11,6 @@ use ethers::{ types::{NameOrAddress, U256}, }; use rustc_hex::ToHex; -use seth::{Seth, SimpleSeth}; use std::{convert::TryFrom, str::FromStr}; use structopt::StructOpt; @@ -19,11 +20,11 @@ async fn main() -> eyre::Result<()> { match opts.sub { Subcommands::FromUtf8 { text } => { let val = unwrap_or_stdin(text)?; - println!("{}", SimpleSeth::from_utf8(&val)); + println!("{}", SimpleCast::from_utf8(&val)); } Subcommands::ToHex { decimal } => { let val = unwrap_or_stdin(decimal)?; - println!("{}", SimpleSeth::hex(U256::from_dec_str(&val)?)); + println!("{}", SimpleCast::hex(U256::from_dec_str(&val)?)); } Subcommands::ToHexdata { input } => { let val = unwrap_or_stdin(input)?; @@ -48,36 +49,36 @@ async fn main() -> eyre::Result<()> { } Subcommands::ToCheckSumAddress { address } => { let val = unwrap_or_stdin(address)?; - println!("{}", SimpleSeth::checksum_address(&val)?); + println!("{}", SimpleCast::checksum_address(&val)?); } Subcommands::ToAscii { hexdata } => { let val = unwrap_or_stdin(hexdata)?; - println!("{}", SimpleSeth::ascii(&val)?); + println!("{}", SimpleCast::ascii(&val)?); } Subcommands::ToBytes32 { bytes } => { let val = unwrap_or_stdin(bytes)?; - println!("{}", SimpleSeth::bytes32(&val)?); + println!("{}", SimpleCast::bytes32(&val)?); } Subcommands::ToDec { hexvalue } => { let val = unwrap_or_stdin(hexvalue)?; - println!("{}", SimpleSeth::to_dec(&val)?); + println!("{}", SimpleCast::to_dec(&val)?); } Subcommands::ToFix { decimals, value } => { let val = unwrap_or_stdin(value)?; println!( "{}", - SimpleSeth::to_fix(unwrap_or_stdin(decimals)?, U256::from_dec_str(&val)?)? + SimpleCast::to_fix(unwrap_or_stdin(decimals)?, U256::from_dec_str(&val)?)? ); } Subcommands::ToUint256 { value } => { let val = unwrap_or_stdin(value)?; - println!("{}", SimpleSeth::to_uint256(&val)?); + println!("{}", SimpleCast::to_uint256(&val)?); } Subcommands::ToWei { value, unit } => { let val = unwrap_or_stdin(value)?; println!( "{}", - SimpleSeth::to_wei( + SimpleCast::to_wei( U256::from_dec_str(&val)?, unit.unwrap_or_else(|| String::from("wei")) )? @@ -85,65 +86,65 @@ async fn main() -> eyre::Result<()> { } Subcommands::Block { rpc_url, block, full, field, to_json } => { let provider = Provider::try_from(rpc_url)?; - println!("{}", Seth::new(provider).block(block, full, field, to_json).await?); + println!("{}", Cast::new(provider).block(block, full, field, to_json).await?); } Subcommands::BlockNumber { rpc_url } => { let provider = Provider::try_from(rpc_url)?; - println!("{}", Seth::new(provider).block_number().await?); + println!("{}", Cast::new(provider).block_number().await?); } Subcommands::Call { rpc_url, address, sig, args } => { let provider = Provider::try_from(rpc_url)?; - println!("{}", Seth::new(provider).call(address, &sig, args).await?); + println!("{}", Cast::new(provider).call(address, &sig, args).await?); } Subcommands::Calldata { sig, args } => { - println!("{}", SimpleSeth::calldata(sig, &args)?); + println!("{}", SimpleCast::calldata(sig, &args)?); } Subcommands::Chain { rpc_url } => { let provider = Provider::try_from(rpc_url)?; - println!("{}", Seth::new(provider).chain().await?); + println!("{}", Cast::new(provider).chain().await?); } Subcommands::ChainId { rpc_url } => { let provider = Provider::try_from(rpc_url)?; - println!("{}", Seth::new(provider).chain_id().await?); + println!("{}", Cast::new(provider).chain_id().await?); } Subcommands::Namehash { name } => { - println!("{}", SimpleSeth::namehash(&name)?); + println!("{}", SimpleCast::namehash(&name)?); } Subcommands::SendTx { eth, to, sig, args } => { let provider = Provider::try_from(eth.rpc_url.as_str())?; if let Some(signer) = eth.signer()? { let from = eth.from.unwrap_or_else(|| signer.address()); let provider = SignerMiddleware::new(provider, signer); - seth_send(provider, from, to, sig, args, eth.seth_async).await?; + cast_send(provider, from, to, sig, args, eth.cast_async).await?; } else { let from = eth.from.expect("No ETH_FROM or signer specified"); - seth_send(provider, from, to, sig, args, eth.seth_async).await?; + cast_send(provider, from, to, sig, args, eth.cast_async).await?; } } Subcommands::Age { block, rpc_url } => { let provider = Provider::try_from(rpc_url)?; println!( "{}", - Seth::new(provider).age(block.unwrap_or(BlockId::Number(Latest))).await? + Cast::new(provider).age(block.unwrap_or(BlockId::Number(Latest))).await? ); } Subcommands::Balance { block, who, rpc_url } => { let provider = Provider::try_from(rpc_url)?; - println!("{}", Seth::new(provider).balance(who, block).await?); + println!("{}", Cast::new(provider).balance(who, block).await?); } Subcommands::BaseFee { block, rpc_url } => { let provider = Provider::try_from(rpc_url)?; println!( "{}", - Seth::new(provider).base_fee(block.unwrap_or(BlockId::Number(Latest))).await? + Cast::new(provider).base_fee(block.unwrap_or(BlockId::Number(Latest))).await? ); } Subcommands::GasPrice { rpc_url } => { let provider = Provider::try_from(rpc_url)?; - println!("{}", Seth::new(provider).gas_price().await?); + println!("{}", Cast::new(provider).gas_price().await?); } Subcommands::Keccak { data } => { - println!("{}", SimpleSeth::keccak(&data)?); + println!("{}", SimpleCast::keccak(&data)?); } Subcommands::ResolveName { who, rpc_url, verify } => { let provider = Provider::try_from(rpc_url)?; @@ -199,23 +200,23 @@ where }) } -async fn seth_send, T: Into>( +async fn cast_send, T: Into>( provider: M, from: F, to: T, sig: String, args: Vec, - seth_async: bool, + cast_async: bool, ) -> eyre::Result<()> where M::Error: 'static, { - let seth = Seth::new(provider); + let cast = Cast::new(provider); let pending_tx = - seth.send(from, to, if !sig.is_empty() { Some((&sig, args)) } else { None }).await?; + cast.send(from, to, if !sig.is_empty() { Some((&sig, args)) } else { None }).await?; let tx_hash = *pending_tx; - if seth_async { + if cast_async { println!("{}", tx_hash); } else { let receipt = pending_tx.await?.ok_or_else(|| eyre::eyre!("tx {} not found", tx_hash))?; diff --git a/dapptools/src/seth_opts.rs b/cli/src/cast_opts.rs similarity index 97% rename from dapptools/src/seth_opts.rs rename to cli/src/cast_opts.rs index 9787a3f20feb..1141189a261a 100644 --- a/dapptools/src/seth_opts.rs +++ b/cli/src/cast_opts.rs @@ -3,7 +3,7 @@ use std::{convert::TryFrom, str::FromStr, sync::Arc}; use ethers::{ providers::{Http, Provider}, signers::{coins_bip39::English, LocalWallet, MnemonicBuilder}, - types::{Address, BlockId, BlockNumber, NameOrAddress, H256, U64}, + types::{Address, BlockId, BlockNumber, NameOrAddress, H256}, }; use eyre::Result; use structopt::StructOpt; @@ -28,7 +28,7 @@ pub enum Subcommands { - @tag, where $TAG is defined in environment variables "#)] ToHexdata { input: Option }, - #[structopt(aliases = &["--to-checksum"])] // Compatibility with dapptools' seth + #[structopt(aliases = &["--to-checksum"])] // Compatibility with dapptools' cast #[structopt(name = "--to-checksum-address")] #[structopt(about = "convert an address to a checksummed format (EIP-55)")] ToCheckSumAddress { address: Option
}, @@ -57,7 +57,7 @@ pub enum Subcommands { Block { #[structopt(help = "the block you want to query, can also be earliest/latest/pending", parse(try_from_str = parse_block_id))] block: BlockId, - #[structopt(long, env = "SETH_FULL_BLOCK")] + #[structopt(long, env = "CAST_FULL_BLOCK")] full: bool, field: Option, #[structopt(long = "--json", short = "-j")] @@ -213,7 +213,7 @@ fn parse_block_id(s: &str) -> eyre::Result { "earliest" => BlockId::Number(BlockNumber::Earliest), "latest" => BlockId::Number(BlockNumber::Latest), s if s.starts_with("0x") => BlockId::Hash(H256::from_str(s)?), - s => BlockId::Number(BlockNumber::Number(U64::from_str(s)?)), + s => BlockId::Number(BlockNumber::Number(u64::from_str(s)?.into())), }) } @@ -245,8 +245,8 @@ pub struct EthereumOpts { #[structopt(env = "ETH_FROM", short, long = "from", help = "The sender account")] pub from: Option
, - #[structopt(long, env = "SETH_ASYNC")] - pub seth_async: bool, + #[structopt(long, env = "CAST_ASYNC")] + pub cast_async: bool, #[structopt(flatten)] pub wallet: Wallet, diff --git a/dapptools/src/cmd/mod.rs b/cli/src/cmd/mod.rs similarity index 100% rename from dapptools/src/cmd/mod.rs rename to cli/src/cmd/mod.rs diff --git a/dapptools/src/cmd/verify.rs b/cli/src/cmd/verify.rs similarity index 97% rename from dapptools/src/cmd/verify.rs rename to cli/src/cmd/verify.rs index bc0a56b511e4..ada213b7587c 100644 --- a/dapptools/src/cmd/verify.rs +++ b/cli/src/cmd/verify.rs @@ -2,6 +2,7 @@ use crate::utils; +use cast::SimpleCast; use ethers::{ abi::{Address, Function, FunctionExt}, core::types::Chain, @@ -10,7 +11,6 @@ use ethers::{ }; use ethers_etherscan::{contract::VerifyContract, Client}; use eyre::ContextCompat; -use seth::SimpleSeth; use std::convert::TryFrom; /// Run the verify command to submit the contract's source code for verification on etherscan @@ -56,7 +56,7 @@ pub async fn run( state_mutability: Default::default(), }; - constructor_args = Some(SimpleSeth::calldata(fun.abi_signature(), &args)?); + constructor_args = Some(SimpleCast::calldata(fun.abi_signature(), &args)?); } else if !args.is_empty() { eyre::bail!("No constructor found but contract arguments provided") } diff --git a/dapptools/src/dapp.rs b/cli/src/forge.rs similarity index 97% rename from dapptools/src/dapp.rs rename to cli/src/forge.rs index 510195c37621..9b4dca185b34 100644 --- a/dapptools/src/dapp.rs +++ b/cli/src/forge.rs @@ -10,15 +10,15 @@ use regex::Regex; use sputnik::backend::Backend; use structopt::StructOpt; -use dapp::MultiContractRunnerBuilder; +use forge::MultiContractRunnerBuilder; use ansi_term::Colour; use ethers::types::U256; -mod dapp_opts; -use dapp_opts::{EvmType, Opts, Subcommands}; +mod forge_opts; +use forge_opts::{EvmType, Opts, Subcommands}; -use crate::dapp_opts::FullContractInfo; +use crate::forge_opts::FullContractInfo; use std::{collections::HashMap, convert::TryFrom, path::Path, sync::Arc}; mod cmd; @@ -215,9 +215,9 @@ fn main() -> eyre::Result<()> { let tree = repo.find_tree(id).unwrap(); let message = if let Some(ref tag) = dep.tag { - format!("turbodapp install: {}\n\n{}", dep.name, tag) + format!("forge install: {}\n\n{}", dep.name, tag) } else { - format!("turbodapp install: {}", dep.name) + format!("forge install: {}", dep.name) }; // committing to the parent may make running the installation step @@ -257,7 +257,7 @@ fn test>( pattern: Regex, json: bool, verbosity: u8, -) -> eyre::Result>> { +) -> eyre::Result>> { let mut runner = builder.build(project, evm)?; let mut exit_code = 0; diff --git a/dapptools/src/dapp_opts.rs b/cli/src/forge_opts.rs similarity index 97% rename from dapptools/src/dapp_opts.rs rename to cli/src/forge_opts.rs index b6b94d7ba366..aa500252966a 100644 --- a/dapptools/src/dapp_opts.rs +++ b/cli/src/forge_opts.rs @@ -13,10 +13,12 @@ pub struct Opts { } #[derive(Debug, StructOpt)] +#[structopt(name = "forge")] #[structopt(about = "Build, test, fuzz, formally verify, debug & deploy solidity contracts.")] #[allow(clippy::large_enum_variant)] pub enum Subcommands { #[structopt(about = "test your smart contracts")] + #[structopt(alias = "t")] Test { #[structopt(help = "print the test results in json format", long, short)] json: bool, @@ -74,10 +76,11 @@ pub enum Subcommands { #[structopt(help = "enables the FFI cheatcode", long)] ffi: bool, - #[structopt(help = "verbosity of 'dapp test' output (0-3)", long, default_value = "0")] + #[structopt(help = "verbosity of 'forge test' output (0-3)", long, default_value = "0")] verbosity: u8, }, #[structopt(about = "build your smart contracts")] + #[structopt(alias = "b")] Build { #[structopt(flatten)] opts: BuildOpts, @@ -168,8 +171,7 @@ impl std::convert::TryFrom<&BuildOpts> for Project { /// Defaults to converting to DAppTools-style repo layout, but can be customized. fn try_from(opts: &BuildOpts) -> eyre::Result { // 1. Set the root dir - let root = opts.root.clone().unwrap_or_else(|| std::env::current_dir().unwrap()); - let root = std::fs::canonicalize(root)?; + let root = std::fs::canonicalize(&opts.root)?; // 2. Set the contracts dir let contracts = if let Some(ref contracts) = opts.contracts { @@ -233,8 +235,12 @@ impl std::convert::TryFrom<&BuildOpts> for Project { #[derive(Debug, StructOpt)] pub struct BuildOpts { - #[structopt(help = "the project's root path, default being the current directory", long)] - pub root: Option, + #[structopt( + help = "the project's root path, default being the current directory", + long, + default_value = "std::env::current_dir().unwrap()" + )] + pub root: PathBuf, #[structopt( help = "the directory relative to the root under which the smart contrats are", @@ -246,8 +252,7 @@ pub struct BuildOpts { #[structopt(help = "the remappings", long, short)] pub remappings: Vec, - - #[structopt(env = "DAPP_REMAPPINGS")] + #[structopt(long = "remappings-env", env = "DAPP_REMAPPINGS")] pub remappings_env: Option, #[structopt(help = "the paths where your libraries are installed", long)] diff --git a/dapptools/src/utils.rs b/cli/src/utils.rs similarity index 100% rename from dapptools/src/utils.rs rename to cli/src/utils.rs diff --git a/dapp/README.md b/dapp/README.md deleted file mode 100644 index ad0980717a3e..000000000000 --- a/dapp/README.md +++ /dev/null @@ -1,167 +0,0 @@ -# `dapp` - -## Run Solidity tests - -Any contract that contains a function starting with `test` is being tested. The glob -passed to `--contracts` must be wrapped with quotes so that it gets passed to the internal -command without being expanded by your shell. - -```bash -$ cargo r --bin dapp test --contracts './**/*.sol' - Finished dev [unoptimized + debuginfo] target(s) in 0.21s - Running `target/debug/dapp test --contracts './**/*.sol'` -Running 1 test for Foo -[PASS] testX (gas: 267) - -Running 1 test for GmTest -[PASS] testGm (gas: 25786) - -Running 1 test for FooBar -[PASS] testX (gas: 267) - -Running 3 tests for GreeterTest -[PASS] testIsolation (gas: 3702) -[PASS] testFailGreeting (gas: 26299) -[PASS] testGreeting (gas: 26223) -``` - -You can optionally specify a regular expression, to only run matching functions: - -```bash -$ cargo r --bin dapp test --contracts './**/*.sol' -m testG - Finished dev [unoptimized + debuginfo] target(s) in 0.26s - Running `target/debug/dapp test --contracts './**/*.sol' -m testG` -Running 1 test for GreeterTest -[PASS] testGreeting (gas: 26223) - -Running 1 test for GmTest -[PASS] testGm (gas: 25786) -``` - -### Test output as JSON - -In order to compose with other commands, you may print the results as JSON via the `--json` flag - -```bash -$ ./target/release/dapp test -c "./**/*.sol" --json -{"GreeterTest":{"testIsolation":{"success":true,"gas_used":3702},"testFailGreeting":{"success":true,"gas_used":26299},"testGreeting":{"success":true,"gas_used":26223}},"FooBar":{"testX":{"success":true,"gas_used":267}},"Foo":{"testX":{"success":true,"gas_used":267}},"GmTest":{"testGm":{"success":true,"gas_used":25786}}} -``` - -### Build the contracts - -You can build the contracts by running, which will by default output the compilation artifacts -of all contracts under `src/` at `out/dapp.sol.json`: - -```bash -$ ./target/release/dapp build -``` - -You can specify an alternative path for your contracts and libraries with `--remappings`, `--lib-path` -and `--contracts`. We default to importing libraries from `./lib`, but you still need to manually -set your remappings. - -In the example below, we see that this also works for importing libraries from different paths -(e.g. having a DappTools-style import under `lib/` and an NPM-style import under `node_modules`) - -Notably, we need 1 remapping and 1 lib path for each import. Given that this can be tedious, -you can do set remappings via the env var `DAPP_REMAPPINGS`, by setting your remapping 1 in each line - -```bash -$ dapp build --out out.json \ - --remappings ds-test/=lib/ds-test/src/ \ - --lib-paths ./lib/ - --remappings @openzeppelin/=node_modules/@openzeppelin/ \ - --lib-path ./node_modules/@openzeppelin -``` - - -```bash -$ echo $DAPP_REMAPPINGS -@openzeppelin/=lib/openzeppelin-contracts/ -ds-test/=lib/ds-test/src/ -$ dapp build --out out.json \ - --lib-paths ./lib/ \ - --lib-paths ./node_modules/@openzeppelin -``` - -### CLI Help - -The CLI options can be seen below. You can fully customize the initial blockchain -context. As an example, if you pass the flag `--block-number`, then the EVM's `NUMBER` -opcode will always return the supplied value. This can be useful for testing. - - -#### Build - -```bash -$ cargo r --bin dapp build --help - Compiling dapptools v0.1.0 - Finished dev [unoptimized + debuginfo] target(s) in 3.45s - Running `target/debug/dapp build --help` -dapp-build 0.1.0 -build your smart contracts - -USAGE: - dapp build [FLAGS] [OPTIONS] [--] [remappings-env] - -FLAGS: - -h, --help Prints help information - -n, --no-compile skip re-compilation - -V, --version Prints version information - -OPTIONS: - -c, --contracts glob path to your smart contracts [default: ./src/**/*.sol] - --evm-version choose the evm version [default: berlin] - --lib-path the path where your libraries are installed - -o, --out path to where the contract artifacts are stored [default: ./out/dapp.sol.json] - -r, --remappings ... the remappings - -ARGS: - [env: DAPP_REMAPPINGS=] -``` - -#### Test - -```bash -$ cargo r --bin dapp test --help - Finished dev [unoptimized + debuginfo] target(s) in 0.31s - Running `target/debug/dapp test --help` -dapp-test 0.1.0 -build your smart contracts - -USAGE: - dapp test [FLAGS] [OPTIONS] [--] [remappings-env] - -FLAGS: - -h, --help Prints help information - -j, --json print the test results in json format - -n, --no-compile skip re-compilation - -V, --version Prints version information - -OPTIONS: - --block-coinbase - the block.coinbase value during EVM execution [default: 0x0000000000000000000000000000000000000000] - - --block-difficulty the block.difficulty value during EVM execution [default: 0] - --block-gas-limit the block.gaslimit value during EVM execution - --block-number the block.number value during EVM execution [default: 0] - --block-timestamp the block.timestamp value during EVM execution [default: 0] - --chain-id the chainid opcode value [default: 1] - -c, --contracts glob path to your smart contracts [default: ./src/**/*.sol] - --evm-version choose the evm version [default: berlin] - --gas-limit the block gas limit [default: 25000000] - --gas-price the tx.gasprice value during EVM execution [default: 0] - --lib-path the path where your libraries are installed - -o, --out - path to where the contract artifacts are stored [default: ./out/dapp.sol.json] - - -m, --match only run test methods matching regex [default: .*] - -r, --remappings ... the remappings - --tx-origin - the tx.origin value during EVM execution [default: 0x0000000000000000000000000000000000000000] - - -ARGS: - [env: DAPP_REMAPPINGS=] - -``` diff --git a/evm-adapters/Cargo.toml b/evm-adapters/Cargo.toml index af8da3dd5318..503a4901165d 100644 --- a/evm-adapters/Cargo.toml +++ b/evm-adapters/Cargo.toml @@ -7,7 +7,7 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dapp-utils = { path = "./../utils" } +foundry-utils = { path = "./../utils" } sputnik = { package = "evm", git = "https://github.com/rust-blockchain/evm", optional = true, features = ["tracing"] } diff --git a/evm-adapters/README.md b/evm-adapters/README.md new file mode 100644 index 000000000000..6f41fa93653d --- /dev/null +++ b/evm-adapters/README.md @@ -0,0 +1,29 @@ +# evm-adapters + +Abstraction over various EVM implementations via the `Evm` trait. Currently +supported: [Sputnik EVM](https://github.com/rust-blockchain/evm/) and +[Evmodin](https://github.com/vorot93/evmodin). + +Any implementation of the EVM trait receives [fuzzing support](./src/fuzz.rs) +using the [`proptest`](https://docs.rs/proptest) crate. + +## Sputnik's Hooked Executor + +In order to implement cheatcodes, we had to hook in EVM execution. This was done +by implementing a `Handler` and overriding the `call` function. + +## Sputnik's Cached Forking backend + +When testing, it is frequently a requirement to be able to fetch live state from +e.g. Ethereum mainnet instead of redeploying the contracts locally yourself. + +To assist with that, we provide 2 forking providers: + +1. ForkMemoryBackend: A simple provider which calls out to the remote node for + any data that it does not have locally, and caching the result to avoid + unnecessary extra requests +1. SharedBackend: A backend which can be cheaply cloned and used in different + tests, typically useful for test parallelization. Under the hood, it has a + background worker which deduplicates any outgoing requests from each + individual backend, while also sharing the return values and cache. This + backend not in-use yet. diff --git a/evm-adapters/src/fuzz.rs b/evm-adapters/src/fuzz.rs index 15a3c3db00c8..498872855f29 100644 --- a/evm-adapters/src/fuzz.rs +++ b/evm-adapters/src/fuzz.rs @@ -75,7 +75,7 @@ impl<'a, S, E: Evm> FuzzedExecutor<'a, E, S> { "{}, expected failure: {}, reason: '{}'", func.name, should_fail, - dapp_utils::decode_revert(returndata.as_ref())? + foundry_utils::decode_revert(returndata.as_ref())? ); Ok(()) diff --git a/evm-adapters/src/lib.rs b/evm-adapters/src/lib.rs index 5fc38dcfc600..2fab1f8deabd 100644 --- a/evm-adapters/src/lib.rs +++ b/evm-adapters/src/lib.rs @@ -17,7 +17,7 @@ use ethers::{ core::types::{Address, Bytes, U256}, }; -use dapp_utils::IntoFunction; +use foundry_utils::IntoFunction; use eyre::Result; use once_cell::sync::Lazy; @@ -75,7 +75,7 @@ pub trait Evm { let func = func.into(); let (retdata, status, gas, logs) = self.call_unchecked(from, to, &func, args, value)?; if Self::is_fail(&status) { - let reason = dapp_utils::decode_revert(retdata.as_ref()).unwrap_or_default(); + let reason = foundry_utils::decode_revert(retdata.as_ref()).unwrap_or_default(); Err(EvmError::Execution { reason, gas_used: gas, logs }) } else { let retdata = decode_function_data(&func, retdata, false)?; diff --git a/dapp/Cargo.toml b/forge/Cargo.toml similarity index 94% rename from dapp/Cargo.toml rename to forge/Cargo.toml index cb8e087a771f..d7f09c0b2e28 100644 --- a/dapp/Cargo.toml +++ b/forge/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "dapp" +name = "forge" version = "0.1.0" edition = "2018" license = "MIT OR Apache-2.0" @@ -7,7 +7,7 @@ license = "MIT OR Apache-2.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dapp-utils = { path = "./../utils" } +foundry-utils = { path = "./../utils" } evm-adapters = { path = "./../evm-adapters", default-features = false } # ethers = { version = "0.5.2" } diff --git a/forge/README.md b/forge/README.md new file mode 100644 index 000000000000..f038357f535d --- /dev/null +++ b/forge/README.md @@ -0,0 +1,193 @@ +# `forge` + +Forge is a fast and flexible Ethereum testing framework, inspired by +[Dapp](https://github.com/dapphub/dapptools/tree/master/src/dapp) + +If you are looking into how to consume the software as an end user, check the +[CLI README](../cli/README.md). + +For more context on how the package works under the hood, look in the +[code docs](./src/lib.rs). + +## Why? + +### Write your tests in Solidity to minimize context switching + +Writing tests in Javascript/Typescript while writing your smart contracts in +Solidity can be confusing. Forge lets you write your tests in Solidity, so you +can focus on what matters. + +```solidity +contract Foo { + uint256 public x = 1; + function set(uint256 _x) external { + x = _x; + } + + function double() external { + x = 2 * x; + } +} + +contract FooTest { + Foo foo; + + // The state of the contract gets reset before each + // test is run, with the `setUp()` function being called + // each time after deployment. + function setUp() public { + foo = new Foo(); + } + + // A simple unit test + function testDouble() public { + require(foo.x() == 1); + foo.double(); + require(foo.x() == 2); + } +} +``` + +### Fuzzing: Go beyond unit testing + +When testing smart contracts, fuzzing can uncover edge cases which would be hard +to manually detect with manual unit testing. We support fuzzing natively, where +any test function that takes >0 arguments will be fuzzed, using the +[proptest](https://docs.rs/proptest/1.0.0/proptest/) crate. + +An example of how a fuzzed test would look like can be seen below: + +```solidity +function testDoubleWithFuzzing(uint256 x) public { + foo.set(x); + require(foo.x() == x); + foo.double(); + require(foo.x() == 2 * x); +} +``` + +## Features + +- [ ] test + - [x] Simple unit tests + - [x] Gas costs + - [x] DappTools style test output + - [x] JSON test output + - [x] Matching on regex + - [x] DSTest-style assertions support + - [x] Fuzzing + - [ ] Symbolic execution + - [ ] Coverage + - [x] HEVM-style Solidity cheatcodes + - [ ] Structured tracing with abi decoding + - [ ] Per-line gas profiling + - [x] Forking mode + - [x] Automatic solc selection +- [x] build + - [x] Can read DappTools-style .sol.json artifacts + - [x] Manual remappings + - [x] Automatic remappings + - [x] Multiple compiler versions + - [x] Incremental compilation + - [ ] Can read Hardhat-style artifacts + - [ ] Can read Truffle-style artifacts +- [x] install +- [x] update +- [ ] debug +- [x] CLI Tracing with `RUST_LOG=forge=trace` + +### Cheat codes + +_The below is modified from +[Dapp's README](https://github.com/dapphub/dapptools/blob/master/src/hevm/README.md#cheat-codes)_ + +We allow modifying blockchain state with "cheat codes". These can be accessed by +calling into a contract at address `0x7109709ECfa91a80626fF3989D68f67F5b1DD12D`, +which implements the following methods: + +- `function warp(uint x) public` Sets the block timestamp to `x`. + +- `function roll(uint x) public` Sets the block number to `x`. + +- `function store(address c, bytes32 loc, bytes32 val) public` Sets the slot + `loc` of contract `c` to `val`. + +- `function load(address c, bytes32 loc) public returns (bytes32 val)` Reads the + slot `loc` of contract `c`. + +- `function sign(uint sk, bytes32 digest) public returns (uint8 v, bytes32 r, bytes32 s)` + Signs the `digest` using the private key `sk`. Note that signatures produced + via `hevm.sign` will leak the private key. + +- `function addr(uint sk) public returns (address addr)` Derives an ethereum + address from the private key `sk`. Note that `hevm.addr(0)` will fail with + `BadCheatCode` as `0` is an invalid ECDSA private key. + +- `function ffi(string[] calldata) external returns (bytes memory)` Executes the + arguments as a command in the system shell and returns stdout. Note that this + cheatcode means test authors can execute arbitrary code on user machines as + part of a call to `dapp test`, for this reason all calls to `ffi` will fail + unless the `--ffi` flag is passed. + +- `function deal(address who, uint256 amount)`: Sets an account's balance + +- `function etch(address where, bytes memory what)`:` Sets the contract code at + some address contract code + +- `function prank(address from, address to, bytes calldata) (bool success,bytes retdata)`: + Performs a smart contract call as another address + +The below example uses the `warp` cheatcode to override the timestamp: + +```solidity +interface Vm { + function warp(uint256 x) external; +} + +contract MyTest { + Vm vm = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); + + function testWarp() public { + vm.warp(100); + require(block.timestamp == 100); + } +``` + +## Future Features + +### Dapptools feature parity + +Over the next months, we intend to add the following features which are +available in upstream dapptools: + +1. Stack Traces: Currently we do not provide any debug information when a call + fails. We intend to add a structured printer (something like + [this](https://twitter.com/gakonst/status/1434337110111182848) which will + show all the calls, logs and arguments passed across intermediate smart + contract calls, which should help with debugging. +1. [Invariant Tests](https://github.com/dapphub/dapptools/blob/master/src/dapp/README.md#invariant-testing) +1. [Interactive Debugger](https://github.com/dapphub/dapptools/blob/master/src/hevm/README.md#interactive-debugger-key-bindings) +1. [Code coverage](https://twitter.com/dapptools/status/1435973810545729536) +1. [Gas snapshots](https://github.com/dapphub/dapptools/pull/850/files) +1. [Symbolic EVM](https://fv.ethereum.org/2020/07/28/symbolic-hevm-release/) + +### Unique features? + +We also intend to add features which are not available in dapptools: + +1. Even faster tests with parallel EVM execution that produces state diffs + instead of modifying the state +1. Improved UX for assertions: + 1. Check revert error or reason on a Solidity call + 1. Check that an event was emitted with expected arguments +1. Support more EVM backends ([revm](https://github.com/bluealloy/revm/), geth's + evm, hevm etc.) & benchmark performance across them +1. Declarative deployment system based on a config file +1. Formatting & Linting (maybe powered by + [Solang](https://github.com/hyperledger-labs/solang)) + 1. `dapp fmt`, an automatic code formatter according to standard rules (like + [`prettier-plugin-solidity`](https://github.com/prettier-solidity/prettier-plugin-solidity)) + 1. `dapp lint`, a linter + static analyzer, like a combination of + [`solhint`](https://github.com/protofire/solhint) and + [slither](https://github.com/crytic/slither/) +1. Flamegraphs for gas profiling diff --git a/dapp/src/lib.rs b/forge/src/lib.rs similarity index 100% rename from dapp/src/lib.rs rename to forge/src/lib.rs diff --git a/dapp/src/multi_runner.rs b/forge/src/multi_runner.rs similarity index 100% rename from dapp/src/multi_runner.rs rename to forge/src/multi_runner.rs diff --git a/dapp/src/runner.rs b/forge/src/runner.rs similarity index 99% rename from dapp/src/runner.rs rename to forge/src/runner.rs index 635cfe4522a0..8b8396d3a39a 100644 --- a/dapp/src/runner.rs +++ b/forge/src/runner.rs @@ -244,11 +244,11 @@ mod tests { mod sputnik { use std::str::FromStr; - use dapp_utils::get_func; use evm_adapters::sputnik::{ helpers::{new_backend, new_vicinity}, Executor, }; + use foundry_utils::get_func; use proptest::test_runner::Config as FuzzConfig; use super::*; diff --git a/dapp/testdata/DebugLogsTest.sol b/forge/testdata/DebugLogsTest.sol similarity index 100% rename from dapp/testdata/DebugLogsTest.sol rename to forge/testdata/DebugLogsTest.sol diff --git a/dapp/testdata/FooTest.sol b/forge/testdata/FooTest.sol similarity index 100% rename from dapp/testdata/FooTest.sol rename to forge/testdata/FooTest.sol diff --git a/dapp/testdata/FooTest2.sol b/forge/testdata/FooTest2.sol similarity index 100% rename from dapp/testdata/FooTest2.sol rename to forge/testdata/FooTest2.sol diff --git a/dapp/testdata/GreetTest.sol b/forge/testdata/GreetTest.sol similarity index 100% rename from dapp/testdata/GreetTest.sol rename to forge/testdata/GreetTest.sol diff --git a/dapp/testdata/dapp-artifact.json b/forge/testdata/dapp-artifact.json similarity index 100% rename from dapp/testdata/dapp-artifact.json rename to forge/testdata/dapp-artifact.json diff --git a/utils/Cargo.toml b/utils/Cargo.toml index 4c2eee5fd8e9..66cdc4f1c690 100644 --- a/utils/Cargo.toml +++ b/utils/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "dapp-utils" +name = "foundry-utils" version = "0.1.0" edition = "2018" license = "MIT OR Apache-2.0" diff --git a/utils/README.md b/utils/README.md new file mode 100644 index 000000000000..45334f7718af --- /dev/null +++ b/utils/README.md @@ -0,0 +1,3 @@ +# foundry-utils + +Helper methods for interacting with EVM ABIs diff --git a/utils/src/lib.rs b/utils/src/lib.rs index 4a4c1c624e97..9d7effffe553 100644 --- a/utils/src/lib.rs +++ b/utils/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] use ethers_core::{ abi::{ self, parse_abi, @@ -42,6 +43,8 @@ impl<'a> IntoFunction for &'a str { } } +/// Given a gas value and a calldata array, it subtracts the calldata cost from the +/// gas value, as well as the 21k base gas cost for all transactions. pub fn remove_extra_costs(gas: U256, calldata: &[u8]) -> U256 { let mut calldata_cost = 0; for i in calldata { @@ -55,6 +58,8 @@ pub fn remove_extra_costs(gas: U256, calldata: &[u8]) -> U256 { gas - calldata_cost - BASE_TX_COST } +/// Given an ABI encoded error string with the function signature `Error(string)`, it decodes +/// it and returns the revert error message. pub fn decode_revert(error: &[u8]) -> std::result::Result { let error = error.strip_prefix(ðers_core::utils::id("Error(string)")).unwrap_or(error); if !error.is_empty() { @@ -64,6 +69,7 @@ pub fn decode_revert(error: &[u8]) -> std::result::Result String { match value { serde_json::Value::String(s) => s, @@ -78,6 +84,7 @@ pub fn to_table(value: serde_json::Value) -> String { } } +/// Given a function signature string, it tries to parse it as a `Function` pub fn get_func(sig: &str) -> Result { // TODO: Make human readable ABI better / more minimal let abi = parse_abi(&[sig])?; @@ -106,6 +113,8 @@ pub fn parse_tokens<'a, I: IntoIterator>( .wrap_err("Failed to parse tokens") } +/// Given a function and a vector of string arguments, it proceeds to convert the args to ethabi +/// Tokens and then ABI encode them. pub fn encode_args(func: &Function, args: &[impl AsRef]) -> Result> { let params = func .inputs