1use crate::{bytecode::VerifyBytecodeArgs, types::VerificationType};
2use alloy_dyn_abi::DynSolValue;
3use alloy_primitives::{Address, Bytes, TxKind, U256};
4use alloy_provider::{
5 Provider,
6 network::{AnyNetwork, AnyRpcBlock},
7};
8use alloy_rpc_types::BlockId;
9use clap::ValueEnum;
10use eyre::{OptionExt, Result};
11use foundry_block_explorers::{
12 contract::{ContractCreationData, ContractMetadata, Metadata},
13 errors::EtherscanError,
14 utils::lookup_compiler_version,
15};
16use foundry_common::{
17 abi::encode_args, compile::ProjectCompiler, ignore_metadata_hash, provider::RetryProvider,
18 shell,
19};
20use foundry_compilers::artifacts::{BytecodeHash, CompactContractBytecode, EvmVersion};
21use foundry_config::Config;
22use foundry_evm::{
23 Env, EnvMut, constants::DEFAULT_CREATE2_DEPLOYER, core::AsEnvMut, executors::TracingExecutor,
24 opts::EvmOpts, traces::TraceMode, utils::apply_chain_and_block_specific_env_changes,
25};
26use foundry_evm_networks::NetworkConfigs;
27use reqwest::Url;
28use revm::{bytecode::Bytecode, database::Database, primitives::hardfork::SpecId};
29use semver::{BuildMetadata, Version};
30use serde::{Deserialize, Serialize};
31use yansi::Paint;
32
33#[derive(Debug, Serialize, Deserialize, Clone, Copy, ValueEnum)]
35pub enum BytecodeType {
36 #[serde(rename = "creation")]
37 Creation,
38 #[serde(rename = "runtime")]
39 Runtime,
40}
41
42impl BytecodeType {
43 pub fn is_creation(&self) -> bool {
45 matches!(self, Self::Creation)
46 }
47
48 pub fn is_runtime(&self) -> bool {
50 matches!(self, Self::Runtime)
51 }
52}
53
54#[derive(Debug, Serialize, Deserialize)]
55pub struct JsonResult {
56 pub bytecode_type: BytecodeType,
57 pub match_type: Option<VerificationType>,
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub message: Option<String>,
60}
61
62pub fn match_bytecodes(
63 local_bytecode: &[u8],
64 bytecode: &[u8],
65 constructor_args: &[u8],
66 is_runtime: bool,
67 bytecode_hash: BytecodeHash,
68) -> Option<VerificationType> {
69 if local_bytecode == bytecode {
71 if bytecode_hash == BytecodeHash::None {
74 return Some(VerificationType::Partial);
75 }
76
77 Some(VerificationType::Full)
78 } else {
79 is_partial_match(local_bytecode, bytecode, constructor_args, is_runtime)
80 .then_some(VerificationType::Partial)
81 }
82}
83
84pub fn build_project(
85 args: &VerifyBytecodeArgs,
86 config: &Config,
87) -> Result<CompactContractBytecode> {
88 let project = config.project()?;
89 let compiler = ProjectCompiler::new();
90
91 let mut output = compiler.compile(&project)?;
92
93 let artifact = output
94 .remove_contract(&args.contract)
95 .ok_or_eyre("Build Error: Contract artifact not found locally")?;
96
97 Ok(artifact.into_contract_bytecode())
98}
99
100pub fn print_result(
101 res: Option<VerificationType>,
102 bytecode_type: BytecodeType,
103 json_results: &mut Vec<JsonResult>,
104 etherscan_config: &Metadata,
105 config: &Config,
106) {
107 if let Some(res) = res {
108 if !shell::is_json() {
109 let _ = sh_println!(
110 "{} with status {}",
111 format!("{bytecode_type:?} code matched").green().bold(),
112 res.green().bold()
113 );
114 } else {
115 let json_res = JsonResult { bytecode_type, match_type: Some(res), message: None };
116 json_results.push(json_res);
117 }
118 } else if !shell::is_json() {
119 let _ = sh_err!(
120 "{bytecode_type:?} code did not match - this may be due to varying compiler settings"
121 );
122 let mismatches = find_mismatch_in_settings(etherscan_config, config);
123 for mismatch in mismatches {
124 let _ = sh_eprintln!("{}", mismatch.red().bold());
125 }
126 } else {
127 let json_res = JsonResult {
128 bytecode_type,
129 match_type: res,
130 message: Some(format!(
131 "{bytecode_type:?} code did not match - this may be due to varying compiler settings"
132 )),
133 };
134 json_results.push(json_res);
135 }
136}
137
138fn is_partial_match(
139 mut local_bytecode: &[u8],
140 mut bytecode: &[u8],
141 constructor_args: &[u8],
142 is_runtime: bool,
143) -> bool {
144 if constructor_args.is_empty() || is_runtime {
146 return try_extract_and_compare_bytecode(local_bytecode, bytecode);
148 }
149
150 bytecode = &bytecode[..bytecode.len() - constructor_args.len()];
152 local_bytecode = &local_bytecode[..local_bytecode.len() - constructor_args.len()];
153
154 try_extract_and_compare_bytecode(local_bytecode, bytecode)
155}
156
157fn try_extract_and_compare_bytecode(mut local_bytecode: &[u8], mut bytecode: &[u8]) -> bool {
158 local_bytecode = ignore_metadata_hash(local_bytecode);
159 bytecode = ignore_metadata_hash(bytecode);
160
161 local_bytecode == bytecode
163}
164
165fn find_mismatch_in_settings(
166 etherscan_settings: &Metadata,
167 local_settings: &Config,
168) -> Vec<String> {
169 let mut mismatches: Vec<String> = vec![];
170 if etherscan_settings.evm_version != local_settings.evm_version.to_string().to_lowercase() {
171 let str = format!(
172 "EVM version mismatch: local={}, onchain={}",
173 local_settings.evm_version, etherscan_settings.evm_version
174 );
175 mismatches.push(str);
176 }
177 let local_optimizer: u64 = if local_settings.optimizer == Some(true) { 1 } else { 0 };
178 if etherscan_settings.optimization_used != local_optimizer {
179 let str = format!(
180 "Optimizer mismatch: local={}, onchain={}",
181 local_settings.optimizer.unwrap_or(false),
182 etherscan_settings.optimization_used
183 );
184 mismatches.push(str);
185 }
186 if local_settings.optimizer_runs.is_some_and(|runs| etherscan_settings.runs != runs as u64)
187 || (local_settings.optimizer_runs.is_none() && etherscan_settings.runs > 0)
188 {
189 let str = format!(
190 "Optimizer runs mismatch: local={}, onchain={}",
191 local_settings.optimizer_runs.map_or("unknown".to_string(), |runs| runs.to_string()),
192 etherscan_settings.runs
193 );
194 mismatches.push(str);
195 }
196
197 mismatches
198}
199
200pub fn maybe_predeploy_contract(
201 creation_data: Result<ContractCreationData, EtherscanError>,
202) -> Result<(Option<ContractCreationData>, bool), eyre::ErrReport> {
203 let mut maybe_predeploy = false;
204 match creation_data {
205 Ok(creation_data) => Ok((Some(creation_data), maybe_predeploy)),
206 Err(EtherscanError::EmptyResult { status, message })
208 if status == "1" && message == "OK" =>
209 {
210 maybe_predeploy = true;
211 Ok((None, maybe_predeploy))
212 }
213 Err(EtherscanError::Serde { error: _, content }) if content.contains("GENESIS") => {
215 maybe_predeploy = true;
216 Ok((None, maybe_predeploy))
217 }
218 Err(e) => eyre::bail!("Error fetching creation data from verifier-url: {:?}", e),
219 }
220}
221
222pub fn check_and_encode_args(
223 artifact: &CompactContractBytecode,
224 args: Vec<String>,
225) -> Result<Vec<u8>, eyre::ErrReport> {
226 if let Some(constructor) = artifact.abi.as_ref().and_then(|abi| abi.constructor()) {
227 if constructor.inputs.len() != args.len() {
228 eyre::bail!(
229 "Mismatch of constructor arguments length. Expected {}, got {}",
230 constructor.inputs.len(),
231 args.len()
232 );
233 }
234 encode_args(&constructor.inputs, &args).map(|args| DynSolValue::Tuple(args).abi_encode())
235 } else {
236 Ok(Vec::new())
237 }
238}
239
240pub fn check_explorer_args(source_code: ContractMetadata) -> Result<Bytes, eyre::ErrReport> {
241 if let Some(args) = source_code.items.first() {
242 Ok(args.constructor_arguments.clone())
243 } else {
244 eyre::bail!("No constructor arguments found from block explorer");
245 }
246}
247
248pub fn check_args_len(
249 artifact: &CompactContractBytecode,
250 args: &Bytes,
251) -> Result<(), eyre::ErrReport> {
252 if let Some(constructor) = artifact.abi.as_ref().and_then(|abi| abi.constructor())
253 && !constructor.inputs.is_empty()
254 && args.is_empty()
255 {
256 eyre::bail!(
257 "Contract expects {} constructor argument(s), but none were provided",
258 constructor.inputs.len()
259 );
260 }
261 Ok(())
262}
263
264pub async fn get_tracing_executor(
265 fork_config: &mut Config,
266 fork_blk_num: u64,
267 evm_version: EvmVersion,
268 evm_opts: EvmOpts,
269) -> Result<(Env, TracingExecutor)> {
270 fork_config.fork_block_number = Some(fork_blk_num);
271 fork_config.evm_version = evm_version;
272
273 let create2_deployer = evm_opts.create2_deployer;
274 let (env, fork, _chain, networks) =
275 TracingExecutor::get_fork_material(fork_config, evm_opts).await?;
276
277 let executor = TracingExecutor::new(
278 env.clone(),
279 fork,
280 Some(fork_config.evm_version),
281 TraceMode::Call,
282 networks,
283 create2_deployer,
284 None,
285 )?;
286
287 Ok((env, executor))
288}
289
290pub fn configure_env_block(env: &mut EnvMut<'_>, block: &AnyRpcBlock, config: NetworkConfigs) {
291 env.block.timestamp = U256::from(block.header.timestamp);
292 env.block.beneficiary = block.header.beneficiary;
293 env.block.difficulty = block.header.difficulty;
294 env.block.prevrandao = Some(block.header.mix_hash.unwrap_or_default());
295 env.block.basefee = block.header.base_fee_per_gas.unwrap_or_default();
296 env.block.gas_limit = block.header.gas_limit;
297 apply_chain_and_block_specific_env_changes::<AnyNetwork>(env.as_env_mut(), block, config);
298}
299
300pub fn deploy_contract(
301 executor: &mut TracingExecutor,
302 env: &Env,
303 spec_id: SpecId,
304 to: Option<TxKind>,
305) -> Result<Address, eyre::ErrReport> {
306 let env = Env::new_with_spec_id(
307 env.evm_env.cfg_env.clone(),
308 env.evm_env.block_env.clone(),
309 env.tx.clone(),
310 spec_id,
311 );
312
313 if to.is_some_and(|to| to.is_call()) {
314 let TxKind::Call(to) = to.unwrap() else { unreachable!() };
315 if to != DEFAULT_CREATE2_DEPLOYER {
316 eyre::bail!(
317 "Transaction `to` address is not the default create2 deployer i.e the tx is not a contract creation tx."
318 );
319 }
320 let result = executor.transact_with_env(env)?;
321
322 trace!(transact_result = ?result.exit_reason);
323 if result.result.len() != 20 {
324 eyre::bail!(
325 "Failed to deploy contract on fork at block: call result is not exactly 20 bytes"
326 );
327 }
328
329 Ok(Address::from_slice(&result.result))
330 } else {
331 let deploy_result = executor.deploy_with_env(env, None)?;
332 trace!(deploy_result = ?deploy_result.raw.exit_reason);
333 Ok(deploy_result.address)
334 }
335}
336
337pub async fn get_runtime_codes(
338 executor: &mut TracingExecutor,
339 provider: &RetryProvider,
340 address: Address,
341 fork_address: Address,
342 block: Option<u64>,
343) -> Result<(Bytecode, Bytes)> {
344 let fork_runtime_code = executor
345 .backend_mut()
346 .basic(fork_address)?
347 .ok_or_else(|| {
348 eyre::eyre!(
349 "Failed to get runtime code for contract deployed on fork at address {}",
350 fork_address
351 )
352 })?
353 .code
354 .ok_or_else(|| {
355 eyre::eyre!(
356 "Bytecode does not exist for contract deployed on fork at address {}",
357 fork_address
358 )
359 })?;
360
361 let onchain_runtime_code = if let Some(block) = block {
362 provider.get_code_at(address).block_id(BlockId::number(block)).await?
363 } else {
364 provider.get_code_at(address).await?
365 };
366
367 Ok((fork_runtime_code, onchain_runtime_code))
368}
369
370pub fn is_host_only(url: &Url) -> bool {
374 matches!(url.path(), "/" | "")
375}
376
377pub async fn ensure_solc_build_metadata(version: Version) -> Result<Version> {
388 if version.build != BuildMetadata::EMPTY {
389 Ok(version)
390 } else {
391 Ok(lookup_compiler_version(&version).await?)
392 }
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398
399 #[test]
400 fn test_host_only() {
401 assert!(!is_host_only(&Url::parse("https://blockscout.net/api").unwrap()));
402 assert!(is_host_only(&Url::parse("https://blockscout.net/").unwrap()));
403 assert!(is_host_only(&Url::parse("https://blockscout.net").unwrap()));
404 }
405}