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