forge_verify/etherscan/
flatten.rs

1use super::{EtherscanSourceProvider, VerifyArgs};
2use crate::provider::VerificationContext;
3use eyre::{Context, Result};
4use foundry_block_explorers::verify::CodeFormat;
5use foundry_compilers::{
6    artifacts::{BytecodeHash, Source, Sources},
7    buildinfo::RawBuildInfo,
8    compilers::{
9        solc::{SolcCompiler, SolcLanguage, SolcVersionedInput},
10        Compiler, CompilerInput,
11    },
12    solc::Solc,
13    AggregatedCompilerOutput,
14};
15use semver::{BuildMetadata, Version};
16use std::path::Path;
17
18#[derive(Debug)]
19pub struct EtherscanFlattenedSource;
20impl EtherscanSourceProvider for EtherscanFlattenedSource {
21    fn source(
22        &self,
23        args: &VerifyArgs,
24        context: &VerificationContext,
25    ) -> Result<(String, String, CodeFormat)> {
26        let metadata = context.project.settings.solc.metadata.as_ref();
27        let bch = metadata.and_then(|m| m.bytecode_hash).unwrap_or_default();
28
29        eyre::ensure!(
30            bch == BytecodeHash::Ipfs,
31            "When using flattened source, bytecodeHash must be set to ipfs because Etherscan uses IPFS in its Compiler Settings when re-compiling your code. BytecodeHash is currently: {}. Hint: Set the bytecodeHash key in your foundry.toml :)",
32            bch,
33        );
34
35        let source = context
36            .project
37            .paths
38            .clone()
39            .with_language::<SolcLanguage>()
40            .flatten(&context.target_path)
41            .wrap_err("Failed to flatten contract")?;
42
43        if !args.force {
44            // solc dry run of flattened code
45            self.check_flattened(source.clone(), &context.compiler_version, &context.target_path)
46                .map_err(|err| {
47                eyre::eyre!(
48                    "Failed to compile the flattened code locally: `{}`\
49            To skip this solc dry, have a look at the `--force` flag of this command.",
50                    err
51                )
52            })?;
53        }
54
55        Ok((source, context.target_name.clone(), CodeFormat::SingleFile))
56    }
57}
58
59impl EtherscanFlattenedSource {
60    /// Attempts to compile the flattened content locally with the compiler version.
61    ///
62    /// This expects the completely flattened content and will try to compile it using the
63    /// provided compiler. If the compiler is missing it will be installed.
64    ///
65    /// # Errors
66    ///
67    /// If it failed to install a missing solc compiler
68    ///
69    /// # Exits
70    ///
71    /// If the solc compiler output contains errors, this could either be due to a bug in the
72    /// flattening code or could to conflict in the flattened code, for example if there are
73    /// multiple interfaces with the same name.
74    fn check_flattened(
75        &self,
76        content: impl Into<String>,
77        version: &Version,
78        contract_path: &Path,
79    ) -> Result<()> {
80        let version = strip_build_meta(version.clone());
81        let solc = Solc::find_or_install(&version)?;
82
83        let input = SolcVersionedInput::build(
84            Sources::from([("contract.sol".into(), Source::new(content))]),
85            Default::default(),
86            SolcLanguage::Solidity,
87            version.clone(),
88        );
89
90        let out = SolcCompiler::Specific(solc).compile(&input)?;
91        if out.errors.iter().any(|e| e.is_error()) {
92            let mut o = AggregatedCompilerOutput::<SolcCompiler>::default();
93            o.extend(version, RawBuildInfo::new(&input, &out, false)?, "default", out);
94            let diags = o.diagnostics(&[], &[], Default::default());
95
96            eyre::bail!(
97                "\
98Failed to compile the flattened code locally.
99This could be a bug, please inspect the output of `forge flatten {}` and report an issue.
100To skip this solc dry, pass `--force`.
101Diagnostics: {diags}",
102                contract_path.display()
103            );
104        }
105
106        Ok(())
107    }
108}
109
110/// Strips [BuildMetadata] from the [Version]
111///
112/// **Note:** this is only for local compilation as a dry run, therefore this will return a
113/// sanitized variant of the specific version so that it can be installed. This is merely
114/// intended to ensure the flattened code can be compiled without errors.
115fn strip_build_meta(version: Version) -> Version {
116    if version.build != BuildMetadata::EMPTY {
117        Version::new(version.major, version.minor, version.patch)
118    } else {
119        version
120    }
121}