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