Repository: gakonst/ethers-rs (rust)

Issue:
[ethers-solc] `pragma experimental` is not stripped on flatten
**Version**
List the versions of all `ethers-rs` crates you are using. The easiest way to get
this information is using `cargo-tree`.

`cargo install cargo-tree`
(see install here: https://github.com/sfackler/cargo-tree)

Then:

`cargo tree | grep ethers`

```
✦ forge --version
forge 0.2.0 (233ab70 2022-04-01T00:48:24.366564+00:00)
```

I think foundry uses `ethers-rs` on master branch.


**Platform**
The output of `uname -a` (UNIX), or version and 32 or 64-bit (Windows)

```
✦ uname -a
Darwin pyk-iMac.local 21.4.0 Darwin Kernel Version 21.4.0: Mon Feb 21 20:36:53 PST 2022; root:xnu-8020.101.4~2/RELEASE_ARM64_T8101 x86_64
```

**Description**
Enter your issue details here.
One way to structure the description:

`pragma experimental` is not stripped on flatten

I tried this code:

```
forge flatten src/Contract.sol > output.sol
```

I expected to see this happen:

`pragma experimental ABIEncoderV2;` will be stripped on each import.

Instead, this happened: 

`pragma experimental ABIEncoderV2;` is not stripped, this issue cause the verify contract on Forge to failed:

```
Error:
   0: Contract verification failed:
      Response: `NOTOK`
      Details: `Fail - Unable to verify`
```

--

https://github.com/gakonst/foundry/issues/852



Pull request:
feat(solc): Strip experimental pragma from all imported contracts
<!--
Thank you for your Pull Request. Please provide a description above and review
the requirements below.

Bug fixes and new features should include tests.

Contributors guide: https://github.com/gakonst/ethers-rs/blob/master/CONTRIBUTING.md

The contributors guide includes instructions for running rustfmt and building the
documentation.
-->

## Motivation

<!--
Explain the context and why you're making that change. What is the problem
you're trying to solve? In some cases there is not a problem and this can be
thought of as being the motivation for your change.
-->

`forge verify-contract` failed because `flatten` doesn't strip the experimental pragma from imported contracts.

Related issues: 
https://github.com/gakonst/foundry/issues/852
https://github.com/gakonst/ethers-rs/issues/1115

## Solution

<!--
Summarize the solution and provide any necessary context needed to understand
the code change.
-->
This PR enable `solc.flatten` to strip pragma from all imported contracts.

Fix #1115 


## PR Checklist

- [x] Added Tests
- [ ] Added Documentation
- [ ] Updated the changelog


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 56e945cf..849bd1a8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2741,9 +2741,9 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.36"
+version = "1.0.37"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
+checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1"
 dependencies = [
  "unicode-xid",
 ]
diff --git a/ethers-core/Cargo.toml b/ethers-core/Cargo.toml
index b6f3b3d4..b95adb50 100644
--- a/ethers-core/Cargo.toml
+++ b/ethers-core/Cargo.toml
@@ -36,7 +36,7 @@ cargo_metadata = { version = "0.14.2", optional = true }
 # eip712 feature enabled dependencies
 convert_case = { version = "0.5.0", optional = true }
 syn = { version = "1.0.91", optional = true }
-proc-macro2 = { version = "1.0.36", optional = true }
+proc-macro2 = { version = "1.0.37", optional = true }
 
 [dev-dependencies]
 serde_json = { version = "1.0.64", default-features = false }
diff --git a/ethers-core/src/types/block.rs b/ethers-core/src/types/block.rs
index 72577900..c738ac99 100644
--- a/ethers-core/src/types/block.rs
+++ b/ethers-core/src/types/block.rs
@@ -210,7 +210,7 @@ impl Serialize for BlockId {
 }
 
 /// A block Number (or tag - "latest", "earliest", "pending")
-#[derive(Copy, Clone, Debug, PartialEq)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
 pub enum BlockNumber {
     /// Latest block
     Latest,
diff --git a/ethers-core/src/types/signature.rs b/ethers-core/src/types/signature.rs
index e322a987..956695c1 100644
--- a/ethers-core/src/types/signature.rs
+++ b/ethers-core/src/types/signature.rs
@@ -131,7 +131,7 @@ impl Signature {
     }
 
     /// Retrieve the recovery ID.
-    fn recovery_id(&self) -> Result<RecoveryId, SignatureError> {
+    pub fn recovery_id(&self) -> Result<RecoveryId, SignatureError> {
         let standard_v = normalize_recovery_id(self.v);
         Ok(RecoveryId::new(standard_v)?)
     }
diff --git a/ethers-solc/src/config.rs b/ethers-solc/src/config.rs
index 6ffda54c..d39db454 100644
--- a/ethers-solc/src/config.rs
+++ b/ethers-solc/src/config.rs
@@ -250,7 +250,7 @@ impl ProjectPathsConfig {
     pub fn flatten(&self, target: &Path) -> Result<String> {
         tracing::trace!("flattening file");
         let graph = Graph::resolve(self)?;
-        self.flatten_node(target, &graph, &mut Default::default(), false, false)
+        self.flatten_node(target, &graph, &mut Default::default(), false, false, false)
     }
 
     /// Flattens a single node from the dependency graph
@@ -260,6 +260,7 @@ impl ProjectPathsConfig {
         graph: &Graph,
         imported: &mut HashSet<usize>,
         strip_version_pragma: bool,
+        strip_experimental_pragma: bool,
         strip_license: bool,
     ) -> Result<String> {
         let target_dir = target.parent().ok_or_else(|| {
@@ -299,9 +300,17 @@ impl ProjectPathsConfig {
             }
         }
 
+        if strip_experimental_pragma {
+            if let Some(experiment) = target_node.experimental() {
+                let (start, end) = experiment.loc_by_offset(offset);
+                content.splice(start..end, std::iter::empty());
+                offset -= (end - start) as isize;
+            }
+        }
+
         for import in imports.iter() {
             let import_path = self.resolve_import(target_dir, import.data())?;
-            let s = self.flatten_node(&import_path, graph, imported, true, true)?;
+            let s = self.flatten_node(&import_path, graph, imported, true, true, true)?;
             let import_content = s.trim().as_bytes();
             let import_content_len = import_content.len() as isize;
             let (start, end) = import.loc_by_offset(offset);
diff --git a/ethers-solc/src/resolver/mod.rs b/ethers-solc/src/resolver/mod.rs
index bb947ae2..a739db59 100644
--- a/ethers-solc/src/resolver/mod.rs
+++ b/ethers-solc/src/resolver/mod.rs
@@ -763,6 +763,10 @@ impl Node {
         &self.data.version
     }
 
+    pub fn experimental(&self) -> &Option<SolDataUnit<String>> {
+        &self.data.experimental
+    }
+
     pub fn license(&self) -> &Option<SolDataUnit<String>> {
         &self.data.license
     }
diff --git a/ethers-solc/src/resolver/parse.rs b/ethers-solc/src/resolver/parse.rs
index eba8b739..028856f9 100644
--- a/ethers-solc/src/resolver/parse.rs
+++ b/ethers-solc/src/resolver/parse.rs
@@ -13,6 +13,7 @@ use std::path::{Path, PathBuf};
 pub struct SolData {
     pub license: Option<SolDataUnit<String>>,
     pub version: Option<SolDataUnit<String>>,
+    pub experimental: Option<SolDataUnit<String>>,
     pub imports: Vec<SolDataUnit<PathBuf>>,
     pub version_req: Option<VersionReq>,
     pub libraries: Vec<SolLibrary>,
@@ -37,6 +38,7 @@ impl SolData {
     /// parsing fails, we'll fall back to extract that info via regex
     pub fn parse(content: &str, file: &Path) -> Self {
         let mut version = None;
+        let mut experimental = None;
         let mut imports = Vec::<SolDataUnit<PathBuf>>::new();
         let mut libraries = Vec::new();
         let mut contracts = Vec::new();
@@ -48,7 +50,12 @@ impl SolData {
                         SourceUnitPart::PragmaDirective(loc, _, pragma, value) => {
                             if pragma.name == "solidity" {
                                 // we're only interested in the solidity version pragma
-                                version = Some(SolDataUnit::new(value.string, loc.into()));
+                                version = Some(SolDataUnit::new(value.string.clone(), loc.into()));
+                            }
+
+                            if pragma.name == "experimental" {
+                                experimental =
+                                    Some(SolDataUnit::new(value.string.clone(), loc.into()));
                             }
                         }
                         SourceUnitPart::ImportDirective(_, import) => {
@@ -106,7 +113,7 @@ impl SolData {
         });
         let version_req = version.as_ref().and_then(|v| Solc::version_req(v.data()).ok());
 
-        Self { version_req, version, imports, license, libraries, contracts }
+        Self { version_req, version, experimental, imports, license, libraries, contracts }
     }
 
     /// Returns `true` if the solidity file associated with this type contains a solidity library


Test patch:
diff --git a/ethers-solc/tests/project.rs b/ethers-solc/tests/project.rs
index 463758c3..fd3498d7 100644
--- a/ethers-solc/tests/project.rs
+++ b/ethers-solc/tests/project.rs
@@ -499,6 +499,61 @@ contract A { }
     );
 }
 
+#[test]
+fn can_flatten_experimental_pragma() {
+    let project = TempProject::dapptools().unwrap();
+
+    let f = project
+        .add_source(
+            "A",
+            r#"
+pragma solidity ^0.8.10;
+pragma experimental ABIEncoderV2;
+import "./C.sol";
+import "./B.sol";
+contract A { }
+"#,
+        )
+        .unwrap();
+
+    project
+        .add_source(
+            "B",
+            r#"
+pragma solidity ^0.8.10;
+pragma experimental ABIEncoderV2;
+import "./C.sol";
+contract B { }
+"#,
+        )
+        .unwrap();
+
+    project
+        .add_source(
+            "C",
+            r#"
+pragma solidity ^0.8.10;
+pragma experimental ABIEncoderV2;
+import "./A.sol";
+contract C { }
+"#,
+        )
+        .unwrap();
+
+    let result = project.flatten(&f).unwrap();
+
+    assert_eq!(
+        result,
+        r#"
+pragma solidity ^0.8.10;
+pragma experimental ABIEncoderV2;
+contract C { }
+contract B { }
+contract A { }
+"#
+    );
+}
+
 #[test]
 fn can_flatten_file_with_duplicates() {
     let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/test-flatten-duplicates");