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