Skip to main content

forge_script/
verify.rs

1use crate::{
2    ScriptArgs, ScriptConfig,
3    build::LinkedBuildData,
4    sequence::{ScriptSequenceKind, get_commit_hash},
5};
6use alloy_primitives::{Address, hex};
7use eyre::{Result, eyre};
8use forge_script_sequence::{AdditionalContract, ScriptSequence};
9use forge_verify::{RetryArgs, VerifierArgs, VerifyArgs, provider::VerificationProviderType};
10use foundry_cli::opts::{EtherscanOpts, ProjectPathOpts};
11use foundry_common::ContractsByArtifact;
12use foundry_compilers::{Project, artifacts::EvmVersion, info::ContractInfo};
13use foundry_config::{Chain, Config};
14use semver::Version;
15
16/// State after we have broadcasted the script.
17/// It is assumed that at this point [BroadcastedState::sequence] contains receipts for all
18/// broadcasted transactions.
19pub struct BroadcastedState {
20    pub args: ScriptArgs,
21    pub script_config: ScriptConfig,
22    pub build_data: LinkedBuildData,
23    pub sequence: ScriptSequenceKind,
24}
25
26impl BroadcastedState {
27    pub async fn verify(self) -> Result<()> {
28        let Self { args, script_config, build_data, mut sequence, .. } = self;
29
30        let verify = VerifyBundle::new(
31            &script_config.config.project()?,
32            &script_config.config,
33            build_data.known_contracts,
34            args.retry,
35            args.verifier,
36        );
37
38        for sequence in sequence.sequences_mut() {
39            verify_contracts(sequence, &script_config.config, verify.clone()).await?;
40        }
41
42        Ok(())
43    }
44}
45
46/// Data struct to help `ScriptSequence` verify contracts on `etherscan`.
47#[derive(Clone)]
48pub struct VerifyBundle {
49    pub num_of_optimizations: Option<usize>,
50    pub known_contracts: ContractsByArtifact,
51    pub project_paths: ProjectPathOpts,
52    pub etherscan: EtherscanOpts,
53    pub retry: RetryArgs,
54    pub verifier: VerifierArgs,
55    pub via_ir: bool,
56}
57
58impl VerifyBundle {
59    pub fn new(
60        project: &Project,
61        config: &Config,
62        known_contracts: ContractsByArtifact,
63        retry: RetryArgs,
64        verifier: VerifierArgs,
65    ) -> Self {
66        let num_of_optimizations =
67            if config.optimizer == Some(true) { config.optimizer_runs } else { None };
68
69        let config_path = config.get_config_path();
70
71        let project_paths = ProjectPathOpts {
72            root: Some(project.paths.root.clone()),
73            contracts: Some(project.paths.sources.clone()),
74            remappings: project.paths.remappings.clone(),
75            remappings_env: None,
76            cache_path: Some(project.paths.cache.clone()),
77            lib_paths: project.paths.libraries.clone(),
78            hardhat: config.profile == Config::HARDHAT_PROFILE,
79            config_path: if config_path.exists() { Some(config_path) } else { None },
80        };
81
82        let via_ir = config.via_ir;
83
84        Self {
85            num_of_optimizations,
86            known_contracts,
87            etherscan: Default::default(),
88            project_paths,
89            retry,
90            verifier,
91            via_ir,
92        }
93    }
94
95    /// Configures the chain and sets the etherscan key, if available
96    pub fn set_chain(&mut self, config: &Config, chain: Chain) {
97        // If dealing with multiple chains, we need to be able to change in between the config
98        // chain_id.
99        self.etherscan.key = config.get_etherscan_api_key(Some(chain));
100        self.etherscan.chain = Some(chain);
101    }
102
103    /// Given a `VerifyBundle` and contract details, it tries to generate a valid `VerifyArgs` to
104    /// use against the `contract_address`.
105    pub fn get_verify_args(
106        &self,
107        contract_address: Address,
108        create2_offset: usize,
109        data: &[u8],
110        libraries: &[String],
111        evm_version: EvmVersion,
112    ) -> Option<VerifyArgs> {
113        for (artifact, contract) in self.known_contracts.iter() {
114            let Some(bytecode) = contract.bytecode() else { continue };
115            // If it's a CREATE2, the tx.data comes with a 32-byte salt in the beginning
116            // of the transaction
117            if data.split_at(create2_offset).1.starts_with(bytecode) {
118                let constructor_args = data.split_at(create2_offset + bytecode.len()).1.to_vec();
119
120                if artifact.source.extension().is_some_and(|e| e.to_str() == Some("vy")) {
121                    warn!("Skipping verification of Vyper contract: {}", artifact.name);
122                    return None;
123                }
124
125                // Strip artifact profile from contract name when creating contract info.
126                let contract = ContractInfo {
127                    path: Some(artifact.source.to_string_lossy().to_string()),
128                    name: artifact
129                        .name
130                        .strip_suffix(&format!(".{}", &artifact.profile))
131                        .unwrap_or_else(|| &artifact.name)
132                        .to_string(),
133                };
134
135                // We strip the build metadata information, since it can lead to
136                // etherscan not identifying it correctly. eg:
137                // `v0.8.10+commit.fc410830.Linux.gcc` != `v0.8.10+commit.fc410830`
138                let version = Version::new(
139                    artifact.version.major,
140                    artifact.version.minor,
141                    artifact.version.patch,
142                );
143
144                let verify = VerifyArgs {
145                    address: contract_address,
146                    contract: Some(contract),
147                    compiler_version: Some(version.to_string()),
148                    constructor_args: Some(hex::encode(constructor_args)),
149                    constructor_args_path: None,
150                    no_auto_detect: false,
151                    use_solc: None,
152                    num_of_optimizations: self.num_of_optimizations,
153                    etherscan: self.etherscan.clone(),
154                    rpc: Default::default(),
155                    flatten: false,
156                    force: false,
157                    skip_is_verified_check: true,
158                    watch: true,
159                    retry: self.retry,
160                    libraries: libraries.to_vec(),
161                    root: None,
162                    verifier: self.verifier.clone(),
163                    via_ir: self.via_ir,
164                    evm_version: Some(evm_version),
165                    show_standard_json_input: false,
166                    guess_constructor_args: false,
167                    compilation_profile: Some(artifact.profile.to_string()),
168                    language: None,
169                    creation_transaction_hash: None,
170                };
171
172                return Some(verify);
173            }
174        }
175        None
176    }
177}
178
179/// Given the broadcast log, it matches transactions with receipts, and tries to verify any
180/// created contract on etherscan.
181async fn verify_contracts(
182    sequence: &mut ScriptSequence,
183    config: &Config,
184    mut verify: VerifyBundle,
185) -> Result<()> {
186    trace!(target: "script", "verifying {} contracts [{}]", verify.known_contracts.len(), sequence.chain);
187
188    verify.set_chain(config, sequence.chain.into());
189
190    if verify.etherscan.has_key() || verify.verifier.verifier != VerificationProviderType::Etherscan
191    {
192        trace!(target: "script", "prepare future verifications");
193
194        let mut future_verifications = Vec::with_capacity(sequence.receipts.len());
195        let mut unverifiable_contracts = vec![];
196
197        // Make sure the receipts have the right order first.
198        sequence.sort_receipts();
199
200        for (receipt, tx) in sequence.receipts.iter_mut().zip(sequence.transactions.iter()) {
201            // create2 hash offset
202            let mut offset = 0;
203
204            if tx.is_create2() {
205                receipt.contract_address = tx.contract_address;
206                offset = 32;
207            }
208
209            // Verify contract created directly from the transaction
210            if let (Some(address), Some(data)) = (receipt.contract_address, tx.tx().input()) {
211                match verify.get_verify_args(
212                    address,
213                    offset,
214                    data,
215                    &sequence.libraries,
216                    config.evm_version,
217                ) {
218                    Some(verify) => future_verifications.push(verify.run()),
219                    None => unverifiable_contracts.push(address),
220                };
221            }
222
223            // Verify potential contracts created during the transaction execution
224            for AdditionalContract { address, init_code, .. } in &tx.additional_contracts {
225                match verify.get_verify_args(
226                    *address,
227                    0,
228                    init_code.as_ref(),
229                    &sequence.libraries,
230                    config.evm_version,
231                ) {
232                    Some(verify) => future_verifications.push(verify.run()),
233                    None => unverifiable_contracts.push(*address),
234                };
235            }
236        }
237
238        trace!(target: "script", "collected {} verification jobs and {} unverifiable contracts", future_verifications.len(), unverifiable_contracts.len());
239
240        check_unverified(sequence, unverifiable_contracts, verify);
241
242        let num_verifications = future_verifications.len();
243        let mut num_of_successful_verifications = 0;
244        sh_println!("##\nStart verification for ({num_verifications}) contracts")?;
245        for verification in future_verifications {
246            match verification.await {
247                Ok(_) => {
248                    num_of_successful_verifications += 1;
249                }
250                Err(err) => {
251                    sh_err!("Failed to verify contract: {err:#}")?;
252                }
253            }
254        }
255
256        if num_of_successful_verifications < num_verifications {
257            return Err(eyre!(
258                "Not all ({num_of_successful_verifications} / {num_verifications}) contracts were verified!"
259            ));
260        }
261
262        sh_println!("All ({num_verifications}) contracts were verified!")?;
263    }
264
265    Ok(())
266}
267
268fn check_unverified(
269    sequence: &ScriptSequence,
270    unverifiable_contracts: Vec<Address>,
271    verify: VerifyBundle,
272) {
273    if !unverifiable_contracts.is_empty() {
274        let _ = sh_warn!(
275            "We haven't found any matching bytecode for the following contracts: {:?}.\n\n\
276            This may occur when resuming a verification, but the underlying source code or compiler version has changed.\n\
277            Run `forge clean` to make sure builds are in sync with project files, then try again. Alternatively, use `forge verify-contract` to verify contracts that are already deployed.",
278            unverifiable_contracts
279        );
280
281        if let Some(commit) = &sequence.commit {
282            let current_commit = verify
283                .project_paths
284                .root
285                .map(|root| get_commit_hash(&root).unwrap_or_default())
286                .unwrap_or_default();
287
288            if &current_commit != commit {
289                let _ = sh_warn!(
290                    "Script was broadcasted on commit `{commit}`, but we are at `{current_commit}`."
291                );
292            }
293        }
294    }
295}