forge_verify/etherscan/
flatten.rs

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