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