Skip to main content

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    AggregatedCompilerOutput,
7    artifacts::{BytecodeHash, Source, Sources},
8    buildinfo::RawBuildInfo,
9    compilers::{
10        Compiler, CompilerInput,
11        solc::{SolcCompiler, SolcLanguage, SolcVersionedInput},
12    },
13    solc::Solc,
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        let compiler = SolcCompiler::Specific(solc);
90
91        let out = compiler.compile(&input)?;
92        let compiler_version = compiler.compiler_version(&input);
93        let compound_version = compound_version(compiler_version, &input.version);
94        if out.errors.iter().any(|e| e.is_error()) {
95            let mut o = AggregatedCompilerOutput::<SolcCompiler>::default();
96            o.extend(
97                version,
98                RawBuildInfo::new(&input, &out, &compound_version, false)?,
99                "default",
100                out,
101            );
102            let diags = o.diagnostics(&[], &[], Default::default());
103
104            eyre::bail!(
105                "\
106Failed to compile the flattened code locally.
107This could be a bug, please inspect the output of `forge flatten {}` and report an issue.
108To skip this solc dry, pass `--force`.
109Diagnostics: {diags}",
110                contract_path.display()
111            );
112        }
113
114        Ok(())
115    }
116}
117
118/// Strips [BuildMetadata] from the [Version]
119///
120/// **Note:** this is only for local compilation as a dry run, therefore this will return a
121/// sanitized variant of the specific version so that it can be installed. This is merely
122/// intended to ensure the flattened code can be compiled without errors.
123fn strip_build_meta(version: Version) -> Version {
124    if version.build != BuildMetadata::EMPTY {
125        Version::new(version.major, version.minor, version.patch)
126    } else {
127        version
128    }
129}
130
131fn compound_version(mut compiler_version: Version, input_version: &Version) -> Version {
132    if compiler_version != *input_version {
133        let build = if compiler_version.build.is_empty() {
134            semver::BuildMetadata::new(&format!(
135                "{}.{}.{}",
136                input_version.major, input_version.minor, input_version.patch,
137            ))
138            .expect("can't fail due to parsing")
139        } else {
140            semver::BuildMetadata::new(&format!(
141                "{}-{}.{}.{}",
142                compiler_version.build.as_str(),
143                input_version.major,
144                input_version.minor,
145                input_version.patch,
146            ))
147            .expect("can't fail due to parsing")
148        };
149        compiler_version.build = build;
150    };
151    compiler_version
152}