diff --git a/README.md b/README.md index f5e929ee..5a30fe0f 100644 --- a/README.md +++ b/README.md @@ -308,9 +308,12 @@ dv validate --continue new.dvf.json ### Update DVF -**Please note: The `update` command is currently only updating existing storage variables in a DVF and might not be suitable for fully updating a DVF to the current state of a smart contract. This behavior will be changed in future releases.** +To update an existing DVF, you have two different options: -To update the values of storage variables in a DVF to the state of the latest block and gather all events up to this block, run the following command: +#. Update the values of existing storage variables in a DVF to the state of the latest block and gather all events up to this block. +#. Update the DVF with all incoming changes starting from the init block and gather all events up to this block. + +For the first option, run the following command: ``` dv update new.dvf.json @@ -324,6 +327,22 @@ dv update --validationblock new.dvf.json `` must be greater than the deployment block of the contract and smaller than or equal to the current block of the smart contract's chain. +For the second option, you can add the `--discover` flag: + +``` +dv update --discover new.dvf.json +``` + +When using the `--discover` flag, you can also specify all the flags related to implementation contracts (since these can change over time): + +``` +dv update --discover --implementation ... new.dvf.json +``` + +See [Proxies and Delegated calls](#proxies-and-delegated-calls) for more details. + +With the `--discover` flag, `dv` updates your DVF with all new storage slots starting from the init block until your chosen validation block. This can result in slots you previously discarded to reappear if the value of the slot changed after the init block. + ### Check Bytecode For a simple check that the compiled bytecode of a certain project is equal to the on-chain bytecode of an address, you can use the following command: @@ -590,7 +609,6 @@ This section will be updated soon. - As detailed [above](#dvf-creation), many public RPCs are not or only partially supported for DVF creation. - Finding the deployment transaction of a contract currently requires either Blockscout or Etherscan API keys to collect all relevant information. - Contracts performing `delegatecall` to more than one other contract are currently not supported. -- `dv update` currently only updates values of existing storage variables in the DVF and does not add newly added storage values. - Multiple contracts with the same name compiled with different compiler versions in one project are not supported. - Multi-dimensional mappings with static keys (e.g., `mapping[1][2]`) can currently not be decoded. - Empty-string mapping keys can currently not be decoded correctly. diff --git a/lib/dvf/discovery.rs b/lib/dvf/discovery.rs new file mode 100644 index 00000000..1694b50c --- /dev/null +++ b/lib/dvf/discovery.rs @@ -0,0 +1,493 @@ +use std::collections::{HashMap, HashSet}; +use std::path::PathBuf; + +use alloy::json_abi::Event; +use alloy::primitives::{Address, B256}; +use alloy_dyn_abi::EventExt; +use alloy_rpc_types::Log; +use clap::ArgMatches; +use colored::Colorize; +use prettytable::{row, Table}; +use tracing::{debug, info}; + +use crate::bytecode_verification::parse_json::{Environment, ProjectInfo}; +use crate::dvf::config::DVFConfig; +use crate::dvf::parse::{self, ValidationError}; +use crate::dvf::registry; +use crate::state::contract_state::ContractState; +use crate::state::forge_inspect; +use crate::utils::pretty::PrettyPrinter; +use crate::utils::progress::{print_progress, ProgressMode}; +use crate::utils::read_write_file::get_project_paths; +use crate::web3; + +pub struct DiscoveryParams<'a> { + pub config: &'a DVFConfig, + pub contract_name: &'a str, + pub address: &'a Address, + pub start_block_num: u64, + pub end_block_num: u64, + pub project: Option<&'a PathBuf>, + pub artifacts: &'a str, + pub env: Environment, + pub build_cache: Option<&'a String>, + pub libraries: Option>, + pub implementation_name: Option<&'a str>, + pub implementation_project: Option<&'a PathBuf>, + pub implementation_env: Environment, + pub implementation_artifacts: &'a str, + pub implementation_build_cache: Option<&'a String>, + pub zerovalue: bool, + pub event_topics: Option>, + pub pc: &'a mut u64, + pub progress_mode: &'a ProgressMode, + pub use_storage_range: bool, +} + +pub struct DiscoveryResult { + pub critical_storage_variables: Vec, + pub critical_events: Vec, + pub storage_var_table: Table, + pub event_table: Table, + pub all_events: Vec, + pub proxy_warning: bool, +} + +pub fn discover_storage_and_events( + params: DiscoveryParams, +) -> Result { + let registry = registry::Registry::from_config(params.config)?; + let pretty_printer = PrettyPrinter::new(params.config, Some(®istry)); + + // Initialize storage layout and types based on project availability + let (mut storage_layout, mut types, mut contract_state) = if let Some(project_path) = + params.project + { + let artifacts_path = get_project_paths(project_path, params.artifacts); + + // Load main project info + let project_info = ProjectInfo::new( + ¶ms.contract_name.to_string(), + project_path, + params.env, + &artifacts_path, + params.build_cache, + params.libraries.clone(), + )?; + + // Load storage layout using forge inspect + let fi_layout = forge_inspect::ForgeInspectLayoutStorage::generate_and_parse_layout( + project_path, + params.contract_name, + project_info.absolute_path.clone(), + ); + let fi_ir = forge_inspect::ForgeInspectIrOptimized::generate_and_parse_ir_optimized( + project_path, + params.contract_name, + project_info.absolute_path.clone(), + ); + let mut contract_state = ContractState::new_with_address(params.address, &pretty_printer); + contract_state.add_forge_inspect(&fi_layout, &fi_ir); + + ( + project_info.storage.clone(), + project_info.types.clone(), + contract_state, + ) + } else { + // Fallback: discovery without layout info + let contract_state = ContractState::new_with_address(params.address, &pretty_printer); + (vec![], HashMap::new(), contract_state) + }; + + // Handle implementation contract if present + let mut imp_project_info: Option = None; + if let Some(implementation_name) = params.implementation_name { + print_progress( + "Obtaining ABI of implementation contract.", + params.pc, + params.progress_mode, + ); + + let imp_path: PathBuf; + let imp_artifacts_path: PathBuf; + if let Some(imp_project) = params.implementation_project { + imp_artifacts_path = get_project_paths(imp_project, params.implementation_artifacts); + imp_path = imp_project.clone(); + } else if let Some(project_path) = params.project { + imp_path = project_path.clone(); + imp_artifacts_path = get_project_paths(project_path, params.artifacts); + } else { + return Err(ValidationError::from( + "Implementation contract specified but no project path provided", + )); + } + + let tmp_project_info = ProjectInfo::new( + &implementation_name.to_string(), + &imp_path, + params.implementation_env, + &imp_artifacts_path, + params.implementation_build_cache, + params.libraries.clone(), + )?; + + print_progress( + "Obtaining storage layout of implementation contract.", + params.pc, + params.progress_mode, + ); + let fi_impl_layout = forge_inspect::ForgeInspectLayoutStorage::generate_and_parse_layout( + &imp_path, + implementation_name, + tmp_project_info.absolute_path.clone(), + ); + let fi_impl_ir = forge_inspect::ForgeInspectIrOptimized::generate_and_parse_ir_optimized( + &imp_path, + implementation_name, + tmp_project_info.absolute_path.clone(), + ); + contract_state.add_forge_inspect(&fi_impl_layout, &fi_impl_ir); + + storage_layout.extend(tmp_project_info.storage.clone()); + types.extend(tmp_project_info.types.clone()); + imp_project_info = Some(tmp_project_info); + } + + // Get transaction hashes based on event topics + let mut seen_events: Vec = vec![]; + let tx_hashes: Vec = if let Some(event_topics) = ¶ms.event_topics { + print_progress( + "Obtaining past events and transactions.", + params.pc, + params.progress_mode, + ); + seen_events = web3::get_eth_events( + params.config, + params.address, + params.start_block_num, + params.end_block_num, + event_topics, + )?; + seen_events + .iter() + .filter_map(|e| e.transaction_hash.map(|h| format!("{h:#x}"))) + .collect() + } else { + print_progress( + "Obtaining past transactions.", + params.pc, + params.progress_mode, + ); + web3::get_all_txs_for_contract( + params.config, + params.address, + params.start_block_num, + params.end_block_num, + )? + }; + + print_progress("Getting storage snapshot.", params.pc, params.progress_mode); + let mut snapshot = web3::StorageSnapshot::from_api( + params.config, + params.address, + params.end_block_num, + &tx_hashes, + params.use_storage_range, + )?; + + print_progress("Getting relevant traces.", params.pc, params.progress_mode); + let mut seen_transactions = HashSet::new(); + let mut missing_traces = false; + + for tx_hash in &tx_hashes { + if seen_transactions.contains(tx_hash) { + continue; + } + seen_transactions.insert(tx_hash); + + let mut found_trace = true; + if let Ok(trace) = web3::get_eth_debug_trace(params.config, tx_hash) { + if contract_state + .record_traces(params.config, vec![trace]) + .is_err() + { + found_trace = false; + missing_traces = true; + } + } else { + found_trace = false; + missing_traces = true; + } + + if !found_trace { + info!("Warning. The trace for {tx_hash} cannot be obtained. Some mapping slots might not be decodable."); + } + } + + if missing_traces { + println!("{}", "Warning. At least one transaction trace could not be obtained. This might result in \"unknown\" storage slots due to undecoded mapping keys.".yellow()) + } + + print_progress("Parsing storage snapshot.", params.pc, params.progress_mode); + let mut storage_var_table = Table::new(); + let critical_storage_variables: Vec = contract_state + .get_critical_storage_variables( + &mut snapshot, + &mut storage_var_table, + &storage_layout, + &types, + params.zerovalue, + )?; + + let proxy_warning = critical_storage_variables + .iter() + .any(|var| var.var_name == "unknown") + && imp_project_info.is_some(); + + // Event discovery logic + if params.event_topics.is_none() { + print_progress("Obtaining past events.", params.pc, params.progress_mode); + seen_events = web3::get_eth_events( + params.config, + params.address, + params.start_block_num, + params.end_block_num, + &vec![], + )?; + } + + let mut covered_events = 0; + let mut event_table = Table::new(); + let mut critical_events: Vec = vec![]; + + print_progress("Decoding events.", params.pc, params.progress_mode); + + // Collect all Event Types, making sure to avoid duplications + let all_events = match &imp_project_info { + None => { + if let Some(project_path) = params.project { + let artifacts_path = get_project_paths(project_path, params.artifacts); + let contract_name_string = params.contract_name.to_string(); + let project_info = ProjectInfo::new( + &contract_name_string, + project_path, + params.env, + &artifacts_path, + params.build_cache, + params.libraries.clone(), + )?; + project_info.events.clone() + } else { + vec![] + } + } + Some(imp_project) => { + let mut set_of_sigs: HashSet = HashSet::new(); + let mut res: Vec = vec![]; + + // Get main project events if available + let main_events = if let Some(project_path) = params.project { + let artifacts_path = get_project_paths(project_path, params.artifacts); + let contract_name_string = params.contract_name.to_string(); + let project_info = ProjectInfo::new( + &contract_name_string, + project_path, + params.env, + &artifacts_path, + params.build_cache, + params.libraries.clone(), + )?; + project_info.events.clone() + } else { + vec![] + }; + + for eventlist in [&main_events, &imp_project.events] { + for event in eventlist { + let sig = event.selector(); + if set_of_sigs.contains(&sig) { + info!( + "Warning. Event {} omitted, as it is already known.", + PrettyPrinter::event_to_string(event) + ); + continue; + } + set_of_sigs.insert(sig); + debug!( + "Adding event {} to list.", + PrettyPrinter::event_to_string(event) + ); + res.push(event.clone()); + } + } + res + } + }; + + for abi_event in &all_events { + let sig = PrettyPrinter::event_to_string(abi_event); + debug!("Found the following event: {}", sig); + let topic0 = abi_event.selector(); + debug!("Topic0: {:?}", topic0); + let mut table_head = false; + + // Collect Occurrences + let mut occurrences: Vec = vec![]; + for seen_event in &seen_events { + if seen_event.topic0() == Some(&topic0) { + let log_inner = &seen_event.inner; + let decoded_event = abi_event.decode_log(log_inner)?; + let pretty_event = + pretty_printer.pretty_event_params(abi_event, &decoded_event, true); + + // Add Event Name to table + if !table_head { + event_table.add_row(row![sig]); + table_head = true; + } + // Add Event Occurrence to table + event_table.add_row(row![format!("- {}", pretty_event)]); + + let occurrence = parse::DVFEventOccurrence { + topics: log_inner.data.topics().to_vec(), + data: log_inner.data.data.clone(), + }; + occurrences.push(occurrence); + covered_events += 1; + } + } + + let event_entry = parse::DVFEventEntry { + sig: sig.clone(), + topic0, + occurrences, + }; + critical_events.push(event_entry); + } + + // Handle unknown events + if covered_events != seen_events.len() { + println!( + "Warning! Saw {} events, but able to decode {}.", + seen_events.len(), + covered_events + ); + let used_topics_0: HashSet = all_events.iter().map(|e| e.selector()).collect(); + let all_topics_0: HashSet = + seen_events.iter().map(|e| *e.topic0().unwrap()).collect(); + for unused_topic in all_topics_0.difference(&used_topics_0) { + // Collect Occurrences + let mut occurrences: Vec = vec![]; + for seen_event in &seen_events { + let log_inner = &seen_event.inner; + if seen_event.topic0() == Some(unused_topic) { + let occurrence = parse::DVFEventOccurrence { + topics: log_inner.data.topics().to_vec(), + data: log_inner.data.data.clone(), + }; + occurrences.push(occurrence); + } + } + let event_entry = parse::DVFEventEntry { + sig: String::from("Unknown Signature"), + topic0: *unused_topic, + occurrences, + }; + critical_events.push(event_entry); + } + } + + Ok(DiscoveryResult { + critical_storage_variables, + critical_events, + storage_var_table, + event_table, + all_events, + proxy_warning, + }) +} + +#[allow(clippy::too_many_arguments)] +pub fn create_discovery_params_for_init<'a>( + config: &'a DVFConfig, + dumped: &'a parse::CompleteDVF, + deployment_block_num: u64, + init_block_num: u64, + project: &'a PathBuf, + artifacts: &'a str, + env: Environment, + build_cache: Option<&'a String>, + libraries: Option>, + zerovalue: bool, + event_topics: Option>, + sub_m: &'a ArgMatches, + pc: &'a mut u64, + progress_mode: &'a ProgressMode, +) -> DiscoveryParams<'a> { + DiscoveryParams { + config, + contract_name: &dumped.contract_name, + address: &dumped.address, + start_block_num: deployment_block_num, + end_block_num: init_block_num, + project: Some(project), + artifacts, + env, + build_cache, + libraries, + implementation_name: sub_m + .get_one::("implementation") + .map(|s| s.as_str()), + implementation_project: sub_m.get_one::("implementationproject"), + implementation_env: env, + implementation_artifacts: sub_m.get_one::("implementationartifacts").unwrap(), + implementation_build_cache: sub_m.get_one::("implementationbuildcache"), + zerovalue, + event_topics, + pc, + progress_mode, + use_storage_range: true, + } +} + +#[allow(clippy::too_many_arguments)] +pub fn create_discovery_params_for_update<'a>( + config: &'a DVFConfig, + updated: &'a parse::CompleteDVF, + validation_block_num: u64, + project: Option<&'a PathBuf>, + artifacts: &'a str, + env: Environment, + build_cache: Option<&'a String>, + libraries: Option>, + zerovalue: bool, + sub_m: &'a ArgMatches, + pc: &'a mut u64, + progress_mode: &'a ProgressMode, +) -> DiscoveryParams<'a> { + DiscoveryParams { + config, + contract_name: &updated.contract_name, + address: &updated.address, + start_block_num: updated.init_block_num + 1, + end_block_num: validation_block_num, + project, + artifacts, + env, + build_cache, + libraries, + implementation_name: sub_m + .get_one::("implementation") + .map(|s| s.as_str()), + implementation_project: sub_m.get_one::("implementationproject"), + implementation_env: *sub_m.get_one::("implementationenv").unwrap(), + implementation_artifacts: sub_m.get_one::("implementationartifacts").unwrap(), + implementation_build_cache: sub_m.get_one::("implementationbuildcache"), + zerovalue, + event_topics: None, // Update mode doesn't filter by event topics + pc, + progress_mode, + use_storage_range: false, // cannot use storage range here as we are only trying to get a subset of the state + } +} diff --git a/lib/dvf/mod.rs b/lib/dvf/mod.rs index 56e72bbe..a6014528 100644 --- a/lib/dvf/mod.rs +++ b/lib/dvf/mod.rs @@ -1,4 +1,5 @@ pub mod abstract_wallet; pub mod config; +pub mod discovery; pub mod parse; pub mod registry; diff --git a/lib/utils/mod.rs b/lib/utils/mod.rs index f3c63c20..40735366 100644 --- a/lib/utils/mod.rs +++ b/lib/utils/mod.rs @@ -1,2 +1,3 @@ pub mod pretty; +pub mod progress; pub mod read_write_file; diff --git a/lib/utils/progress.rs b/lib/utils/progress.rs new file mode 100644 index 00000000..1d1b32a4 --- /dev/null +++ b/lib/utils/progress.rs @@ -0,0 +1,26 @@ +pub enum ProgressMode { + Init, + InitProxy, + Update, + UpdateFull, + Validation, + BytecodeCheck, + GenerateBuildCache, + ListEvents, +} + +pub fn print_progress(s: &str, i: &mut u64, pm: &ProgressMode) { + use console::style; + let total = match pm { + ProgressMode::InitProxy => 14, + ProgressMode::Init => 12, + ProgressMode::Update => 4, + ProgressMode::UpdateFull => 11, + ProgressMode::Validation => 5, + ProgressMode::BytecodeCheck => 3, + ProgressMode::GenerateBuildCache => 1, + ProgressMode::ListEvents => 1, + }; + println!("{} {}", style(format!("[{i:2}/{total:2}]")).bold().dim(), s); + *i += 1; +} diff --git a/lib/utils/read_write_file.rs b/lib/utils/read_write_file.rs index 046114a5..1600d64d 100644 --- a/lib/utils/read_write_file.rs +++ b/lib/utils/read_write_file.rs @@ -1,6 +1,6 @@ use std::fs::File; use std::io::Read; -use std::path::Path; +use std::path::{Path, PathBuf}; pub fn list_files(outputs_path: &Path, aux_files: &mut Vec) { if outputs_path.is_dir() { @@ -19,3 +19,11 @@ pub fn read_file(json_path: &Path) -> String { data } + +pub fn get_project_paths(project: &std::path::Path, artifacts: &str) -> PathBuf { + let build_info_dir = "build-info"; + let mut artifacts_path = project.to_path_buf(); + artifacts_path.push(artifacts); + artifacts_path.push(build_info_dir); + artifacts_path +} diff --git a/lib/web3.rs b/lib/web3.rs index 2767a3f6..db93918c 100644 --- a/lib/web3.rs +++ b/lib/web3.rs @@ -1396,11 +1396,15 @@ impl StorageSnapshot { address: &Address, init_block_num: u64, tx_hashes: &Vec, + use_storage_range: bool, ) -> Result { // First try special call - let snapshot: HashMap = if let Ok(storage_snapshot) = - get_eth_storage_snapshot(config, address, init_block_num) - { + let snapshot = if use_storage_range { + Some(get_eth_storage_snapshot(config, address, init_block_num)) + } else { + None + }; + let snapshot: HashMap = if let Some(Ok(storage_snapshot)) = snapshot { /* Self::validate_snapshot_with_mpt_root( config, diff --git a/src/dvf.rs b/src/dvf.rs index 9d4c0cc0..eed752d6 100644 --- a/src/dvf.rs +++ b/src/dvf.rs @@ -4,22 +4,22 @@ use std::path::{Path, PathBuf}; use std::process::exit; use std::str::FromStr; -use alloy::json_abi::Event; use alloy::primitives::{Address, B256}; -use alloy_dyn_abi::EventExt; -use alloy_rpc_types::Log; use clap::{arg, value_parser, ArgMatches, Command}; use colored::Colorize; -use console::style; use dvf_libs::bytecode_verification::compare_bytecodes::{CompareBytecode, CompareInitCode}; use dvf_libs::bytecode_verification::parse_json::{Environment, ProjectInfo}; use dvf_libs::bytecode_verification::verify_bytecode; use dvf_libs::dvf::config::{replace_tilde, DVFConfig}; +use dvf_libs::dvf::discovery::{ + create_discovery_params_for_init, create_discovery_params_for_update, + discover_storage_and_events, DiscoveryResult, +}; use dvf_libs::dvf::parse::{self, DVFStorageEntry, ValidationError, CURRENT_VERSION_STRING}; use dvf_libs::dvf::registry::{self, Registry}; -use dvf_libs::state::contract_state::ContractState; -use dvf_libs::state::forge_inspect::{self, StateVariable, TypeDescription}; use dvf_libs::utils::pretty::PrettyPrinter; +use dvf_libs::utils::progress::{print_progress, ProgressMode}; +use dvf_libs::utils::read_write_file::get_project_paths; use dvf_libs::web3; use indicatif::ProgressBar; use prettytable::{row, Table}; @@ -183,6 +183,7 @@ fn validate_dvf( &pretty_printer, storage_variable, ¤t_val[start_index..end_index], + true, ); if continue_on_mismatch { mismatch_found = true; @@ -546,6 +547,72 @@ fn main() { .help("The block number used for validation") .value_parser(is_valid_blocknum), ) + .arg( + arg!(--discover) + .help( + "Also discover new storage variables and events" + ).action(clap::ArgAction::SetTrue) + ) + .arg( + arg!(--project ) + .help("Path to the root folder of the source code project (optional, improves storage layout discovery)") + .value_parser(is_valid_path) + .requires("discover"), + ) + .arg( + arg!(--artifacts ) + .help("Relative path to the compilation artifacts") + .default_value("artifacts") + .requires("discover"), + ) + .arg( + arg!(--buildcache ) + .help("Build cache, if you have a very large project") + .requires("discover"), + ) + .arg( + arg!(--libraries ) + .help("Library linking information (address mapping)") + .value_parser(value_parser!(String)) + .action(clap::ArgAction::Append) + .requires("discover"), + ) + .arg( + arg!(--env ) + .help("The compile environment") + .value_parser(value_parser!(Environment)) + .default_value("foundry") + .requires("discover"), + ) + .arg( + arg!(--implementation ) + .help("Optional name of the implementation contract") + .requires("discover"), + ) + .arg( + arg!(--implementationproject ) + .help("Path to the root folder of the implementation project") + .value_parser(is_valid_path) + .requires("discover"), + ) + .arg( + arg!(--implementationenv ) + .help("Implementation project's development environment") + .value_parser(value_parser!(Environment)) + .default_value("foundry") + .requires("discover"), + ) + .arg( + arg!(--implementationartifacts ) + .help("Folder containing the implementation project artifacts") + .default_value("artifacts") + .requires("discover"), + ) + .arg( + arg!(--implementationbuildcache ) + .help("Folder containing the implementation contract's build-info files") + .requires("discover"), + ) .arg( arg!(--zerovalue) .help( @@ -708,16 +775,6 @@ fn main() { }; } -enum ProgressMode { - Init, - InitProxy, - Update, - Validation, - BytecodeCheck, - GenerateBuildCache, - ListEvents, -} - fn updated_filename(original_path: &Path) -> PathBuf { // Extract the directory and name let parent = original_path.parent().unwrap_or_else(|| Path::new("")); @@ -737,24 +794,11 @@ fn updated_filename(original_path: &Path) -> PathBuf { updated_path } -fn print_progress(s: &str, i: &mut u64, pm: &ProgressMode) { - let total = match pm { - ProgressMode::InitProxy => 14, - ProgressMode::Init => 12, - ProgressMode::Update => 4, - ProgressMode::Validation => 5, - ProgressMode::BytecodeCheck => 3, - ProgressMode::GenerateBuildCache => 1, - ProgressMode::ListEvents => 1, - }; - println!("{} {}", style(format!("[{i:2}/{total:2}]")).bold().dim(), s); - *i += 1; -} - fn get_mismatch_msg( pretty_printer: &PrettyPrinter, storage_variable: &DVFStorageEntry, current_value_slice: &[u8], + display_mismatch: bool, ) -> String { let var_type = storage_variable.var_type.clone().unwrap_or_default(); let dec_current_value_slice = pretty_printer.pretty_value_short_from_bytes( @@ -765,8 +809,15 @@ fn get_mismatch_msg( let dec_old_value = pretty_printer.pretty_value_short_from_bytes(&var_type, &storage_variable.value, true); + let msg = if display_mismatch { + "Value mismatch" + } else { + "Updated value" + }; + format!( - "Value mismatch for {} (slot {:#x}, offset {}).\nNew value: 0x{} Decoded: {}\nOperator: {}\nOld value: 0x{} Decoded: {}", + "{} for {} (slot {:#x}, offset {}).\nNew value: 0x{} Decoded: {}\nOperator: {}\nOld value: 0x{} Decoded: {}", + msg, storage_variable.var_name, storage_variable.slot, storage_variable.offset, @@ -778,17 +829,6 @@ fn get_mismatch_msg( ) } -fn get_project_paths(project: &Path, artifacts: &str) -> PathBuf { - // no way to access other clap arguments during argument parsing so we have to verify - // artifacts paths here - let build_info_dir = "build-info"; - let mut artifacts_path = project.to_path_buf(); - artifacts_path.push(artifacts); - artifacts_path.push(build_info_dir); - - artifacts_path -} - fn process(matches: ArgMatches) -> Result<(), ValidationError> { let mut config = DVFConfig::from_matches(&matches)?; // Check which subcommand was used @@ -807,25 +847,10 @@ fn process(matches: ArgMatches) -> Result<(), ValidationError> { let event_topics = sub_m .get_many::>("eventtopics") .map(|v| v.flat_map(|x| x.clone()).collect::>()); + let zerovalue = sub_m.get_flag("zerovalue"); let user_deployment_tx = sub_m.get_one::("deployment"); - let mut imp_env = *sub_m.get_one::("implementationenv").unwrap(); - let imp_project = sub_m.get_one::("implementationproject"); - let mut imp_build_cache = sub_m.get_one::("implementationbuildcache"); - let imp_artifacts = sub_m.get_one::("implementationartifacts").unwrap(); - let imp_path: PathBuf; - let imp_artifacts_path: PathBuf; - if let Some(imp_project) = imp_project { - imp_artifacts_path = get_project_paths(imp_project, imp_artifacts); - imp_path = imp_project.clone(); - } else { - imp_path = project.clone(); - imp_artifacts_path = artifacts_path.clone(); - imp_build_cache = build_cache; - imp_env = env - } - let user_output_path = Path::new(sub_m.get_one::("OUTPUT").unwrap()); // This is just a file name so we will place it in the configured folder let output_path: &Path = if is_filename_only_path(user_output_path) { @@ -942,274 +967,34 @@ fn process(matches: ArgMatches) -> Result<(), ValidationError> { debug!("Copying parsed constructor arguments to dvf file"); dumped.copy_constructor_args(&project_info, &pretty_printer); - let mut seen_events: Vec = vec![]; - let tx_hashes: Vec = if let Some(event_topics) = event_topics.clone() { - print_progress( - "Obtaining past events and transactions.", - &mut pc, - &progress_mode, - ); - seen_events = web3::get_eth_events( - &config, - &dumped.address, - deployment_block_num, - init_block_num, - &event_topics, - )?; - seen_events - .iter() - .filter_map(|e| e.transaction_hash.map(|h| format!("{:#x}", h))) - .collect() - } else { - print_progress("Obtaining past transactions.", &mut pc, &progress_mode); - web3::get_all_txs_for_contract( - &config, - &dumped.address, - deployment_block_num, - init_block_num, - )? - }; - - print_progress("Getting storage snapshot.", &mut pc, &progress_mode); - let mut snapshot = web3::StorageSnapshot::from_api( + let DiscoveryResult { + critical_storage_variables, + critical_events, + storage_var_table, + event_table, + all_events, + proxy_warning, + } = discover_storage_and_events(create_discovery_params_for_init( &config, - &dumped.address, + &dumped, + deployment_block_num, init_block_num, - &tx_hashes, - )?; - - if init_block_num < deployment_block_num { - return Err(ValidationError::Error(format!( - "Deployment Block {} is bigger than snapshot block {}.", - deployment_block_num, init_block_num - ))); - } - - print_progress("Obtaining storage layout.", &mut pc, &progress_mode); - // Fetch storage layout - let fi_layout = forge_inspect::ForgeInspectLayoutStorage::generate_and_parse_layout( project, - &dumped.contract_name, - project_info.absolute_path.clone(), - ); - let fi_ir = forge_inspect::ForgeInspectIrOptimized::generate_and_parse_ir_optimized( - project, - &dumped.contract_name, - project_info.absolute_path.clone(), - ); - let mut contract_state = - ContractState::new_with_address(&dumped.address, &pretty_printer); - contract_state.add_forge_inspect(&fi_layout, &fi_ir); - - // Proxy Mode - let mut storage: Vec = project_info.storage.clone(); - let mut types: HashMap = project_info.types.clone(); - let mut imp_project_info: Option = None; - if let Some(implementation_name) = sub_m.get_one::("implementation") { - print_progress( - "Obtaining ABI of implementation contract.", - &mut pc, - &progress_mode, - ); - let tmp_project_info = ProjectInfo::new( - &implementation_name.to_string(), - &imp_path, - imp_env, - &imp_artifacts_path, - imp_build_cache, - libraries, - )?; - - print_progress( - "Obtaining storage layout of implementation contract.", - &mut pc, - &progress_mode, - ); - let fi_impl_layout = - forge_inspect::ForgeInspectLayoutStorage::generate_and_parse_layout( - &imp_path, - implementation_name, - tmp_project_info.absolute_path.clone(), - ); - let fi_impl_ir = - forge_inspect::ForgeInspectIrOptimized::generate_and_parse_ir_optimized( - &imp_path, - implementation_name, - tmp_project_info.absolute_path.clone(), - ); - contract_state.add_forge_inspect(&fi_impl_layout, &fi_impl_ir); - - storage.extend(tmp_project_info.storage.clone()); - types.extend(tmp_project_info.types.clone()); - imp_project_info = Some(tmp_project_info); - } - print_progress("Getting relevant traces.", &mut pc, &progress_mode); - let mut seen_transactions = HashSet::new(); - let mut missing_traces = false; - for tx_hash in &tx_hashes { - if seen_transactions.contains(tx_hash) { - continue; - } - seen_transactions.insert(tx_hash); - info!("Getting trace for {}", tx_hash); - let mut found_trace = true; - if let Ok(trace) = web3::get_eth_debug_trace(&config, tx_hash) { - if contract_state.record_traces(&config, vec![trace]).is_err() { - found_trace = false; - missing_traces = true; - } - } else { - found_trace = false; - missing_traces = true; - } - if !found_trace { - info!("Warning. The trace for {tx_hash} cannot be obtained. Some mapping slots might not be decodable. You can try to increase the timeout in the config."); - } - } - - if missing_traces { - println!("{}", "Warning. At least one transaction trace could not be obtained. This might result in \"unknown\" storage slots due to undecoded mapping keys.".yellow()) - } - - print_progress("Parsing storage snapshot.", &mut pc, &progress_mode); - let mut storage_var_table = Table::new(); - let critical_storage_variables: Vec = contract_state - .get_critical_storage_variables( - &mut snapshot, - &mut storage_var_table, - &storage, - &types, - zerovalue, - )?; - - let mut proxy_warning = critical_storage_variables - .iter() - .any(|var| var.var_name == "unknown"); + artifacts, + env, + build_cache, + libraries.clone(), + zerovalue, + event_topics, + sub_m, + &mut pc, + &progress_mode, + ))?; dumped.critical_storage_variables = critical_storage_variables; - - let mut critical_events: Vec = vec![]; - - if event_topics.is_none() { - print_progress("Obtaining past events.", &mut pc, &progress_mode); - seen_events = web3::get_eth_events( - &config, - &dumped.address, - deployment_block_num, - init_block_num, - &vec![], - )?; - } - - let mut covered_events = 0; - let mut event_table = Table::new(); - - print_progress("Decoding events.", &mut pc, &progress_mode); - - // Collect all Event Types, making sure to avoid duplications - // Event does not implement PartialEq - let all_events = match &imp_project_info { - None => project_info.events.clone(), - Some(imp_project) => { - let mut set_of_sigs: HashSet = HashSet::new(); - let mut res: Vec = vec![]; - for eventlist in [&project_info.events, &imp_project.events] { - for event in eventlist { - let sig = event.selector(); - if set_of_sigs.contains(&sig) { - info!( - "Warning. Event {} omitted, as it is already known.", - PrettyPrinter::event_to_string(event) - ); - continue; - } - set_of_sigs.insert(sig); - debug!( - "Adding event {} to list.", - PrettyPrinter::event_to_string(event) - ); - - res.push(event.clone()); - } - } - res - } - }; - for abi_event in &all_events { - let sig = PrettyPrinter::event_to_string(abi_event); - debug!("Found the following event: {}", sig); - let topic0 = abi_event.selector(); - debug!("Topic0: {:?}", topic0); - let mut table_head = false; - - // Collect Occurrences - let mut occurrences: Vec = vec![]; - for seen_event in &seen_events { - if seen_event.topic0() == Some(&topic0) { - let log_inner = &seen_event.inner; - let decoded_event = abi_event.decode_log(log_inner)?; - let pretty_event = - pretty_printer.pretty_event_params(abi_event, &decoded_event, true); - - // Add Event Name to table - if !table_head { - event_table.add_row(row![sig]); - table_head = true; - } - // Add Event Occurrence to table - event_table.add_row(row![format!("- {}", pretty_event)]); - - let occurrence = parse::DVFEventOccurrence { - topics: log_inner.data.topics().to_vec(), - data: log_inner.data.data.clone(), - }; - occurrences.push(occurrence); - covered_events += 1; - } - } - - let event_entry = parse::DVFEventEntry { - sig: sig.clone(), - topic0, - occurrences, - }; - critical_events.push(event_entry); - } - if covered_events != seen_events.len() { - proxy_warning = true; - println!( - "Warning! Saw {} events, but able to decode {}.", - seen_events.len(), - covered_events - ); - let used_topics_0: HashSet = - all_events.iter().map(|e| e.selector()).collect(); - let all_topics_0: HashSet = - seen_events.iter().map(|e| *e.topic0().unwrap()).collect(); - for unused_topic in all_topics_0.difference(&used_topics_0) { - // Collect Occurrences - let mut occurrences: Vec = vec![]; - for seen_event in &seen_events { - let log_inner = &seen_event.inner; - if seen_event.topic0() == Some(unused_topic) { - let occurrence = parse::DVFEventOccurrence { - topics: log_inner.data.topics().to_vec(), - data: log_inner.data.data.clone(), - }; - occurrences.push(occurrence); - } - } - let event_entry = parse::DVFEventEntry { - sig: String::from("Unknown Signature"), - topic0: *unused_topic, - occurrences, - }; - critical_events.push(event_entry); - } - } dumped.critical_events = critical_events; - pc = 1; + let mut pc = 1; println!(); println!("DVF Initialization complete. Please follow these steps:"); @@ -1218,9 +1003,9 @@ fn process(matches: ArgMatches) -> Result<(), ValidationError> { "{}. Warning. You are using an old compiler without storage layout. There will be no storage decoding.", pc ); pc += 1; - } else if proxy_warning && imp_project_info.is_none() { + } else if proxy_warning && sub_m.get_one::("implementation").is_none() { println!( - "{}. Warning. Not everything could be decoded. This could be because this is a proxy contract. In that case use --implementation to decode more.", pc + "{}. Warning. Some storage slots could not be decoded. This might happen because this is a proxy contract. In that case, use --implementation to decode more.", pc ); pc += 1; } @@ -1400,7 +1185,16 @@ fn process(matches: ArgMatches) -> Result<(), ValidationError> { println!("input path {}", input_path.display()); let mut pc = 1_u64; - let progress_mode = ProgressMode::Update; + + let discover = sub_m.get_flag("discover"); + println!("running discover mode? {}", discover); + + let progress_mode = if discover { + ProgressMode::UpdateFull + } else { + ProgressMode::Update + }; + print_progress("Loading file.", &mut pc, &progress_mode); let filled = parse::CompleteDVF::from_path(&input_path)?; @@ -1435,91 +1229,212 @@ fn process(matches: ArgMatches) -> Result<(), ValidationError> { } print_progress("Checking Storage Variables.", &mut pc, &progress_mode); - // Validate Storage slots - for storage_variable in updated.critical_storage_variables.iter_mut() { - let current_val = web3::get_eth_storage_at( - &config, - &filled.address, - &storage_variable.slot, - validation_block_num, - )?; - let size: usize = storage_variable.value.len(); - let start_index: usize = 32 - (storage_variable.offset + size); - let end_index: usize = 32 - storage_variable.offset; - if current_val[start_index..end_index] != storage_variable.value { - let registry = registry::Registry::from_config(&config)?; - let pretty_printer = PrettyPrinter::new(&config, Some(®istry)); - println!( - "{}", - get_mismatch_msg( - &pretty_printer, - storage_variable, - ¤t_val[start_index..end_index] - ) - ); - storage_variable.value = current_val[start_index..end_index].to_vec(); - storage_variable.value_hint = None; + + if discover { + let discovery_result = + discover_storage_and_events(create_discovery_params_for_update( + &config, + &updated, + validation_block_num, + sub_m.get_one::("project"), + sub_m.get_one::("artifacts").unwrap(), + *sub_m.get_one::("env").unwrap(), + sub_m.get_one::("buildcache"), + sub_m + .get_many::("libraries") + .map(|vals| vals.cloned().collect()), + zerovalue, + sub_m, + &mut pc, + &progress_mode, + ))?; + + updated.init_block_num = validation_block_num; + + // Update existing storage variables and add new ones + let current_storage_map: HashMap = + discovery_result + .critical_storage_variables + .iter() + .map(|var| (format!("{:#x}", var.slot), var)) + .collect(); + + // Check for changes in existing storage variables + for storage_variable in updated.critical_storage_variables.iter_mut() { + let slot_key = format!("{:#x}", storage_variable.slot); + if let Some(current_var) = current_storage_map.get(&slot_key) { + if current_var.value != storage_variable.value { + let registry = registry::Registry::from_config(&config)?; + let pretty_printer = PrettyPrinter::new(&config, Some(®istry)); + println!( + "{}", + get_mismatch_msg( + &pretty_printer, + storage_variable, + ¤t_var.value, + false + ) + ); + storage_variable.value = current_var.value.clone(); + storage_variable.value_hint = current_var.value_hint.clone(); + } + } } - } - if !zerovalue { - // Remove storage variables with value 0 - updated + + // Add new storage variables + let existing_slots: HashSet<_> = updated .critical_storage_variables - .retain(|var| !var.is_zero()); - } + .iter() + .map(|var| var.slot) + .collect(); - print_progress("Checking Events.", &mut pc, &progress_mode); - // Validate events - for critical_event in updated.critical_events.iter_mut() { - let seen_events = web3::get_eth_events( - &config, - &filled.address, - filled.deployment_block_num, - validation_block_num, - &vec![critical_event.topic0], - )?; - let mut replace_events = false; - if seen_events.len() != critical_event.occurrences.len() { - println!( - "Old DVF had {} occurrences of event {}, but new should have {}.", - critical_event.occurrences.len(), - critical_event.sig, - seen_events.len() - ); - replace_events = true; + for new_var in discovery_result.critical_storage_variables { + if !existing_slots.contains(&new_var.slot) { + println!( + "Found new storage variable: {} at slot {}", + new_var.var_name, new_var.slot + ); + updated.critical_storage_variables.push(new_var); + } } - let num_shared = std::cmp::min(seen_events.len(), critical_event.occurrences.len()); - #[allow(clippy::needless_range_loop)] - for i in 0..num_shared { - let log_innner = &seen_events[i].inner; - if log_innner.topics() != critical_event.occurrences[i].topics { + // Update events similarly + let current_events_map: HashMap = discovery_result + .critical_events + .iter() + .map(|event| (event.topic0, event)) + .collect(); + + // Check for changes in existing events + for critical_event in updated.critical_events.iter_mut() { + if let Some(current_event) = current_events_map.get(&critical_event.topic0) { + if current_event.occurrences.len() != critical_event.occurrences.len() { + println!( + "Event {} occurrence count changed from {} to {}", + critical_event.sig, + critical_event.occurrences.len(), + current_event.occurrences.len() + ); + critical_event.occurrences = current_event.occurrences.clone(); + } + } + } + + // Add new events + let existing_topics: HashSet<_> = updated + .critical_events + .iter() + .map(|event| event.topic0) + .collect(); + + for new_event in discovery_result.critical_events { + if !existing_topics.contains(&new_event.topic0) { println!( - "Mismatching topics for event occurrence {} of {}.", - i, critical_event.sig + "Found new event: {} with {} occurrences", + new_event.sig, + new_event.occurrences.len() ); - replace_events = true; + updated.critical_events.push(new_event); } - if log_innner.data.data != critical_event.occurrences[i].data { + } + } else { + // Fallback: manual storage checking without project info (original approach) + for storage_variable in updated.critical_storage_variables.iter_mut() { + let current_val = web3::get_eth_storage_at( + &config, + &filled.address, + &storage_variable.slot, + validation_block_num, + )?; + let size: usize = storage_variable.value.len(); + let start_index: usize = 32 - (storage_variable.offset + size); + let end_index: usize = 32 - storage_variable.offset; + if current_val[start_index..end_index] != storage_variable.value { + let registry = registry::Registry::from_config(&config)?; + let pretty_printer = PrettyPrinter::new(&config, Some(®istry)); println!( - "Mismatching data for event occurrence {} of {}.", - i, critical_event.sig + "{}", + get_mismatch_msg( + &pretty_printer, + storage_variable, + ¤t_val[start_index..end_index], + false + ) ); - replace_events = true; + storage_variable.value = current_val[start_index..end_index].to_vec(); + + if let Some(var_type) = &storage_variable.var_type { + storage_variable.value_hint = + Some(pretty_printer.pretty_value_short_from_bytes( + var_type, + &storage_variable.value, + false, + )); + } else { + storage_variable.value_hint = None; + } } } - if replace_events { - // Collect Occurrences - let mut occurrences: Vec = vec![]; - for seen_event in &seen_events { - let log_inner = &seen_event.inner; - let occurrence = parse::DVFEventOccurrence { - topics: log_inner.data.topics().to_vec(), - data: log_inner.data.data.clone(), - }; - occurrences.push(occurrence); + if !zerovalue { + // Remove storage variables with value 0 + updated + .critical_storage_variables + .retain(|var| !var.is_zero()); + } + print_progress("Checking Events.", &mut pc, &progress_mode); + // Validate events + for critical_event in updated.critical_events.iter_mut() { + let seen_events = web3::get_eth_events( + &config, + &filled.address, + filled.deployment_block_num, + validation_block_num, + &vec![critical_event.topic0], + )?; + let mut replace_events = false; + if seen_events.len() != critical_event.occurrences.len() { + println!( + "Old DVF had {} occurrences of event {}, but new should have {}.", + critical_event.occurrences.len(), + critical_event.sig, + seen_events.len() + ); + replace_events = true; + } + + let num_shared = + std::cmp::min(seen_events.len(), critical_event.occurrences.len()); + #[allow(clippy::needless_range_loop)] + for i in 0..num_shared { + let log_innner = &seen_events[i].inner; + if log_innner.topics() != critical_event.occurrences[i].topics { + println!( + "Mismatching topics for event occurrence {} of {}.", + i, critical_event.sig + ); + replace_events = true; + } + if log_innner.data.data != critical_event.occurrences[i].data { + println!( + "Mismatching data for event occurrence {} of {}.", + i, critical_event.sig + ); + replace_events = true; + } + } + if replace_events { + // Collect Occurrences + let mut occurrences: Vec = vec![]; + for seen_event in &seen_events { + let log_inner = &seen_event.inner; + let occurrence = parse::DVFEventOccurrence { + topics: log_inner.data.topics().to_vec(), + data: log_inner.data.data.clone(), + }; + occurrences.push(occurrence); + } + critical_event.occurrences = occurrences; } - critical_event.occurrences = occurrences; } } updated.clear_id(); @@ -1534,13 +1449,16 @@ fn process(matches: ArgMatches) -> Result<(), ValidationError> { break; } } + updated.generate_id()?; updated.write_to_file(&output_path)?; println!("Wrote the updated file to file: {}", output_path.display()); println!( "{}: Arrays are not properly supported in the update mode.", "Warning".yellow() ); - println!("Note that 'update' will just update existing storage variables and events. If new critical variables or events were introduced, they need to be added manually."); + if discover { + println!("Note: For better storage variable naming and value hints, consider using --project and / or -- implementationproject to provide the source code path."); + } Ok(()) } Some(("generate-config", _sub_m)) => { diff --git a/tests/Contracts/script/Deploy_ProxyUpgrade.s.sol b/tests/Contracts/script/Deploy_ProxyUpgrade.s.sol new file mode 100644 index 00000000..52a9e5c1 --- /dev/null +++ b/tests/Contracts/script/Deploy_ProxyUpgrade.s.sol @@ -0,0 +1,43 @@ +pragma solidity ^0.8.12; + +import "forge-std/Script.sol"; +import { + TransparentUpgradeableProxy as TransparentUpgradeableProxy2, + ITransparentUpgradeableProxy +} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol"; +import "../src/MyToken.sol"; + +contract S is Script { + uint256 x; + uint256 y; + + function run() external { + uint256 anvilDefaultKey = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80; + //uint256 ganacheDefaultKey = 0x0cc0c2de7e8c30525b4ca3b9e0b9703fb29569060d403261055481df7014f7fa; + vm.startBroadcast(anvilDefaultKey); + MyToken m = new MyToken(); + bytes memory payload = abi.encodeWithSignature("initialize()"); + address admin = vm.addr(anvilDefaultKey); + TransparentUpgradeableProxy2 p = new TransparentUpgradeableProxy2(address(m), admin, payload); + MyToken real = MyToken(address(p)); + for (uint256 i = 0; i < 20; i++) { + real.dummy(); + // Waste some time here + for (uint256 i = 0; i < 10; i++) { + y = x; + } + } + + // load admin contract from storage slot + ProxyAdmin proxyAdmin = ProxyAdmin(address(bytes20(vm.load(address(p), ERC1967Utils.ADMIN_SLOT) << 96))); + MyTokenV2 m2 = new MyTokenV2(); + proxyAdmin.upgradeAndCall( + ITransparentUpgradeableProxy(address(p)), address(m2), abi.encodeCall(MyTokenV2.initialize, ()) + ); + real.dummy(); + + vm.stopBroadcast(); + } +} diff --git a/tests/Contracts/src/MyToken.sol b/tests/Contracts/src/MyToken.sol index 5f943bdd..0c28cbf2 100644 --- a/tests/Contracts/src/MyToken.sol +++ b/tests/Contracts/src/MyToken.sol @@ -914,10 +914,26 @@ contract MyToken is Initializable, ERC20Upgradeable { _disableInitializers(); } - function initialize() public initializer { + function initialize() public virtual initializer { __ERC20_init("MyToken", "MTK"); _mint(msg.sender, 10 ** 19); } - function dummy() external {} + function dummy() external virtual {} +} + +contract MyTokenV2 is MyToken { + event DummyEvent(); + + uint256 public dummyValue; + + /// @custom:oz-upgrades-unsafe-allow constructor + function initialize() public override reinitializer(2) { + __ERC20_init("MyTokenV2", "MTKV2"); + } + + function dummy() external override { + dummyValue = 42; + emit DummyEvent(); + } } diff --git a/tests/expected_dvfs/Deploy_0_updated.dvf.json b/tests/expected_dvfs/Deploy_0_updated.dvf.json index d00b7992..255a675f 100644 --- a/tests/expected_dvfs/Deploy_0_updated.dvf.json +++ b/tests/expected_dvfs/Deploy_0_updated.dvf.json @@ -35,6 +35,7 @@ "var_name": "x[0x48656c6c6f207468697320697320612074657374]", "var_type": "t_uint256", "value": "0x0000000000000000000000000000000000000000000000000000000000000006", + "value_hint": "6", "comparison_operator": "Equal" } ], diff --git a/tests/expected_dvfs/Deploy_1_updated.dvf.json b/tests/expected_dvfs/Deploy_1_updated.dvf.json index d8865090..02b2571c 100644 --- a/tests/expected_dvfs/Deploy_1_updated.dvf.json +++ b/tests/expected_dvfs/Deploy_1_updated.dvf.json @@ -53,6 +53,7 @@ "var_name": "x[Hello this is a test]", "var_type": "t_uint256", "value": "0x0000000000000000000000000000000000000000000000000000000000000006", + "value_hint": "6", "comparison_operator": "Equal" }, { diff --git a/tests/expected_dvfs/Deploy_3_updated.dvf.json b/tests/expected_dvfs/Deploy_3_updated.dvf.json index 7d444371..d55d6079 100644 --- a/tests/expected_dvfs/Deploy_3_updated.dvf.json +++ b/tests/expected_dvfs/Deploy_3_updated.dvf.json @@ -17,6 +17,7 @@ "var_name": "x", "var_type": "t_uint256", "value": "0x00000000000000000000000000000000000000000000000000000000000001c8", + "value_hint": "456", "comparison_operator": "Equal" }, { @@ -25,6 +26,7 @@ "var_name": "S.A", "var_type": "t_uint256", "value": "0x0000000000000000000000000000000000000000000000000000000000000040", + "value_hint": "64", "comparison_operator": "Equal" }, { diff --git a/tests/expected_dvfs/Deploy_4_updated.dvf.json b/tests/expected_dvfs/Deploy_4_updated.dvf.json index 63ebf8a7..ad80e67a 100644 --- a/tests/expected_dvfs/Deploy_4_updated.dvf.json +++ b/tests/expected_dvfs/Deploy_4_updated.dvf.json @@ -17,6 +17,7 @@ "var_name": "staticDynamic[0].length", "var_type": "t_uint256", "value": "0x0000000000000000000000000000000000000000000000000000000000000003", + "value_hint": "3", "comparison_operator": "Equal" }, { diff --git a/tests/expected_dvfs/Deploy_6_updated.dvf.json b/tests/expected_dvfs/Deploy_6_updated.dvf.json index 31b7cfa4..7872f749 100644 --- a/tests/expected_dvfs/Deploy_6_updated.dvf.json +++ b/tests/expected_dvfs/Deploy_6_updated.dvf.json @@ -17,6 +17,7 @@ "var_name": "s", "var_type": "t_uint8", "value": "0x03", + "value_hint": "3", "comparison_operator": "Equal" } ], diff --git a/tests/expected_dvfs/MyTokenUpgrade.dvf.json b/tests/expected_dvfs/MyTokenUpgrade.dvf.json new file mode 100644 index 00000000..000e150d --- /dev/null +++ b/tests/expected_dvfs/MyTokenUpgrade.dvf.json @@ -0,0 +1,88 @@ +{ + "version": "0.9.1", + "id": "0xea582136017acdbdec6da968a7c63c78c13e25b9d435e7fd72fb64a75ff786e6", + "contract_name": "MyToken", + "address": "0x5fbdb2315678afecb367f032d93f642f64180aa3", + "chain_id": 1337, + "deployment_block_num": 2, + "init_block_num": 3, + "deployment_tx": "0x42d8e1a6dc338ecb42716c59dce82a58e56c067dff2a20975f28e05970e89400", + "codehash": "0xa77e382f36db7714068fa97e6bc080fbc6b04a900a3199ab1aba56c9dea88f4d", + "insecure": false, + "immutables": [ + { + "var_name": "router", + "value": "0x000000000000000000000000e592427a0aece92de3edee1f18e0157c05861564" + }, + { + "var_name": "penalty", + "value": "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc", + "value_hint": "-4" + } + ], + "constructor_args": [], + "critical_storage_variables": [ + { + "slot": "0x0", + "offset": 0, + "var_name": "_initialized", + "var_type": "t_uint8", + "value": "0xff", + "value_hint": "255", + "comparison_operator": "Equal" + }, + { + "slot": "0x36", + "offset": 0, + "var_name": "_name (length=0)", + "var_type": "t_string_storage", + "value": "0x0000000000000000000000000000000000000000000000000000000000000000", + "value_hint": "", + "comparison_operator": "Equal" + }, + { + "slot": "0x37", + "offset": 0, + "var_name": "_symbol (length=0)", + "var_type": "t_string_storage", + "value": "0x0000000000000000000000000000000000000000000000000000000000000000", + "value_hint": "", + "comparison_operator": "Equal" + } + ], + "critical_events": [ + { + "sig": "Approval(address,address,uint256)", + "topic0": "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925", + "occurrences": [] + }, + { + "sig": "Initialized(uint8)", + "topic0": "0x7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498", + "occurrences": [ + { + "topics": [ + "0x7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498" + ], + "data": "0x00000000000000000000000000000000000000000000000000000000000000ff" + } + ] + }, + { + "sig": "Transfer(address,address,uint256)", + "topic0": "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "occurrences": [] + } + ], + "unvalidated_metadata": { + "author_name": "Author", + "description": "System Description", + "hardfork": [ + "paris", + "shanghai" + ], + "audit_report": "https://example.org/report.pdf", + "source_url": "https://github.com/source/code", + "security_contact": "security@example.org" + } +} \ No newline at end of file diff --git a/tests/expected_dvfs/MyTokenUpgradeV2.dvf.json b/tests/expected_dvfs/MyTokenUpgradeV2.dvf.json new file mode 100644 index 00000000..b0367043 --- /dev/null +++ b/tests/expected_dvfs/MyTokenUpgradeV2.dvf.json @@ -0,0 +1,93 @@ +{ + "version": "0.9.1", + "id": "0x4507300ee38c240f2878be5c0c0f4883663a15b3e4ec7d399038349ff9184a5c", + "contract_name": "MyTokenV2", + "address": "0x4ed7c70f96b99c776995fb64377f0d4ab3b0e1c1", + "chain_id": 1337, + "deployment_block_num": 24, + "init_block_num": 24, + "deployment_tx": "0xdf934a0314e91dd194a691f7c1df94df2a7a4076cd9e83c56c4cc69aa242c333", + "codehash": "0x10bb9b391821a504974d6b6431907bc83e338fdc3c61e3e09a0dde60f56e5072", + "insecure": false, + "immutables": [ + { + "var_name": "router", + "value": "0x000000000000000000000000e592427a0aece92de3edee1f18e0157c05861564" + }, + { + "var_name": "penalty", + "value": "0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc", + "value_hint": "-4" + } + ], + "constructor_args": [], + "critical_storage_variables": [ + { + "slot": "0x0", + "offset": 0, + "var_name": "_initialized", + "var_type": "t_uint8", + "value": "0xff", + "value_hint": "255", + "comparison_operator": "Equal" + }, + { + "slot": "0x36", + "offset": 0, + "var_name": "_name (length=0)", + "var_type": "t_string_storage", + "value": "0x0000000000000000000000000000000000000000000000000000000000000000", + "value_hint": "", + "comparison_operator": "Equal" + }, + { + "slot": "0x37", + "offset": 0, + "var_name": "_symbol (length=0)", + "var_type": "t_string_storage", + "value": "0x0000000000000000000000000000000000000000000000000000000000000000", + "value_hint": "", + "comparison_operator": "Equal" + } + ], + "critical_events": [ + { + "sig": "Approval(address,address,uint256)", + "topic0": "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925", + "occurrences": [] + }, + { + "sig": "DummyEvent()", + "topic0": "0x321d83059d08b108790a2ca1fd77d843211cdfff5dc3b5d7fb4574be368d4497", + "occurrences": [] + }, + { + "sig": "Initialized(uint8)", + "topic0": "0x7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498", + "occurrences": [ + { + "topics": [ + "0x7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498" + ], + "data": "0x00000000000000000000000000000000000000000000000000000000000000ff" + } + ] + }, + { + "sig": "Transfer(address,address,uint256)", + "topic0": "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "occurrences": [] + } + ], + "unvalidated_metadata": { + "author_name": "Author", + "description": "System Description", + "hardfork": [ + "paris", + "shanghai" + ], + "audit_report": "https://example.org/report.pdf", + "source_url": "https://github.com/source/code", + "security_contact": "security@example.org" + } +} \ No newline at end of file diff --git a/tests/expected_dvfs/TransparentUpgradeableProxyUpgrade.dvf.json b/tests/expected_dvfs/TransparentUpgradeableProxyUpgrade.dvf.json new file mode 100644 index 00000000..9762ed5d --- /dev/null +++ b/tests/expected_dvfs/TransparentUpgradeableProxyUpgrade.dvf.json @@ -0,0 +1,165 @@ +{ + "version": "0.9.1", + "id": "0x8091eaf511afa2caa8a44c2e359299ea560f608d3f207dd16f8c75d4ad591b43", + "contract_name": "TransparentUpgradeableProxy", + "address": "0xe7f1725e7734ce288f8367e1bb143e90bb3f0512", + "chain_id": 1337, + "deployment_block_num": 3, + "init_block_num": 3, + "deployment_tx": "0xafab58d1b40c949aa4680c09e8012680d2d52708b49ad9666a206ec8a03b531b", + "codehash": "0x07a3f582ceb15f50e21a238db7f4aab8320b4fac8d78379d05feaf2721f139b1", + "insecure": false, + "immutables": [ + { + "var_name": "_admin", + "value": "0x000000000000000000000000cafac3dd18ac6c6e92c921884f9e4176737c052c" + } + ], + "constructor_args": [ + { + "var_name": "_logic", + "value": "0x5fbdb2315678afecb367f032d93f642f64180aa3" + }, + { + "var_name": "initialOwner", + "value": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" + }, + { + "var_name": "_data", + "value": "0x8129fc1c" + } + ], + "critical_storage_variables": [ + { + "slot": "0x0", + "offset": 0, + "var_name": "_initialized", + "var_type": "t_uint8", + "value": "0x01", + "value_hint": "1", + "comparison_operator": "Equal" + }, + { + "slot": "0x35", + "offset": 0, + "var_name": "_totalSupply", + "var_type": "t_uint256", + "value": "0x0000000000000000000000000000000000000000000000008ac7230489e80000", + "value_hint": "1. * 10^19", + "comparison_operator": "Equal" + }, + { + "slot": "0x36", + "offset": 0, + "var_name": "_name (length=7)", + "var_type": "t_string_storage", + "value": "0x4d79546f6b656e0000000000000000000000000000000000000000000000000e", + "value_hint": "MyToken", + "comparison_operator": "Equal" + }, + { + "slot": "0x37", + "offset": 0, + "var_name": "_symbol (length=3)", + "var_type": "t_string_storage", + "value": "0x4d544b0000000000000000000000000000000000000000000000000000000006", + "value_hint": "MTK", + "comparison_operator": "Equal" + }, + { + "slot": "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", + "offset": 0, + "var_name": "StorageSlot.IMPLEMENTATION_SLOT.AddressSlot.value", + "var_type": "t_address", + "value": "0x5fbdb2315678afecb367f032d93f642f64180aa3", + "comparison_operator": "Equal" + }, + { + "slot": "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103", + "offset": 0, + "var_name": "StorageSlot.ADMIN_SLOT.AddressSlot.value", + "var_type": "t_address", + "value": "0xcafac3dd18ac6c6e92c921884f9e4176737c052c", + "comparison_operator": "Equal" + }, + { + "slot": "0xf6d04bbe1a75429862aa97cc198c639a5559580c07ae94016a2b25986f2e3abd", + "offset": 0, + "var_name": "_balances[0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266]", + "var_type": "t_uint256", + "value": "0x0000000000000000000000000000000000000000000000008ac7230489e80000", + "value_hint": "1. * 10^19", + "comparison_operator": "Equal" + } + ], + "critical_events": [ + { + "sig": "AdminChanged(address,address)", + "topic0": "0x7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f", + "occurrences": [ + { + "topics": [ + "0x7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cafac3dd18ac6c6e92c921884f9e4176737c052c" + } + ] + }, + { + "sig": "Upgraded(address)", + "topic0": "0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b", + "occurrences": [ + { + "topics": [ + "0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b", + "0x0000000000000000000000005fbdb2315678afecb367f032d93f642f64180aa3" + ], + "data": "0x" + } + ] + }, + { + "sig": "Approval(address,address,uint256)", + "topic0": "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925", + "occurrences": [] + }, + { + "sig": "Initialized(uint8)", + "topic0": "0x7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498", + "occurrences": [ + { + "topics": [ + "0x7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000000001" + } + ] + }, + { + "sig": "Transfer(address,address,uint256)", + "topic0": "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "occurrences": [ + { + "topics": [ + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266" + ], + "data": "0x0000000000000000000000000000000000000000000000008ac7230489e80000" + } + ] + } + ], + "unvalidated_metadata": { + "author_name": "Author", + "description": "System Description", + "hardfork": [ + "paris", + "shanghai" + ], + "audit_report": "https://example.org/report.pdf", + "source_url": "https://github.com/source/code", + "security_contact": "security@example.org", + "implementation_name": "MyToken" + } +} \ No newline at end of file diff --git a/tests/expected_dvfs/TransparentUpgradeableProxyUpgrade_updated.dvf.json b/tests/expected_dvfs/TransparentUpgradeableProxyUpgrade_updated.dvf.json new file mode 100644 index 00000000..c399fbcf --- /dev/null +++ b/tests/expected_dvfs/TransparentUpgradeableProxyUpgrade_updated.dvf.json @@ -0,0 +1,170 @@ +{ + "version": "0.9.1", + "id": "0xdf8305c2fe8f3b111189989267e5845cb90d271206f6a152ba5fd7736f3da7a6", + "contract_name": "TransparentUpgradeableProxy", + "address": "0xe7f1725e7734ce288f8367e1bb143e90bb3f0512", + "chain_id": 1337, + "deployment_block_num": 3, + "init_block_num": 26, + "deployment_tx": "0xafab58d1b40c949aa4680c09e8012680d2d52708b49ad9666a206ec8a03b531b", + "codehash": "0x07a3f582ceb15f50e21a238db7f4aab8320b4fac8d78379d05feaf2721f139b1", + "insecure": false, + "immutables": [ + { + "var_name": "_admin", + "value": "0x000000000000000000000000cafac3dd18ac6c6e92c921884f9e4176737c052c" + } + ], + "constructor_args": [ + { + "var_name": "_logic", + "value": "0x5fbdb2315678afecb367f032d93f642f64180aa3" + }, + { + "var_name": "initialOwner", + "value": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" + }, + { + "var_name": "_data", + "value": "0x8129fc1c" + } + ], + "critical_storage_variables": [ + { + "slot": "0x0", + "offset": 0, + "var_name": "_initialized", + "var_type": "t_uint8", + "value": "0x02", + "value_hint": "2", + "comparison_operator": "Equal" + }, + { + "slot": "0x35", + "offset": 0, + "var_name": "_totalSupply", + "var_type": "t_uint256", + "value": "0x0000000000000000000000000000000000000000000000008ac7230489e80000", + "value_hint": "1. * 10^19", + "comparison_operator": "Equal" + }, + { + "slot": "0x36", + "offset": 0, + "var_name": "_name (length=7)", + "var_type": "t_string_storage", + "value": "0x4d79546f6b656e56320000000000000000000000000000000000000000000012", + "value_hint": "MyTokenV2", + "comparison_operator": "Equal" + }, + { + "slot": "0x37", + "offset": 0, + "var_name": "_symbol (length=3)", + "var_type": "t_string_storage", + "value": "0x4d544b563200000000000000000000000000000000000000000000000000000a", + "value_hint": "MTKV2", + "comparison_operator": "Equal" + }, + { + "slot": "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", + "offset": 0, + "var_name": "StorageSlot.IMPLEMENTATION_SLOT.AddressSlot.value", + "var_type": "t_address", + "value": "0x4ed7c70f96b99c776995fb64377f0d4ab3b0e1c1", + "comparison_operator": "Equal" + }, + { + "slot": "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103", + "offset": 0, + "var_name": "StorageSlot.ADMIN_SLOT.AddressSlot.value", + "var_type": "t_address", + "value": "0xcafac3dd18ac6c6e92c921884f9e4176737c052c", + "comparison_operator": "Equal" + }, + { + "slot": "0xf6d04bbe1a75429862aa97cc198c639a5559580c07ae94016a2b25986f2e3abd", + "offset": 0, + "var_name": "_balances[0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266]", + "var_type": "t_uint256", + "value": "0x0000000000000000000000000000000000000000000000008ac7230489e80000", + "value_hint": "1. * 10^19", + "comparison_operator": "Equal" + }, + { + "slot": "0x65", + "offset": 0, + "var_name": "dummyValue", + "var_type": "t_uint256", + "value": "0x000000000000000000000000000000000000000000000000000000000000002a", + "value_hint": "42", + "comparison_operator": "Equal" + } + ], + "critical_events": [ + { + "sig": "AdminChanged(address,address)", + "topic0": "0x7e644d79422f17c01e4894b5f4f588d331ebfa28653d42ae832dc59e38c9798f", + "occurrences": [] + }, + { + "sig": "Upgraded(address)", + "topic0": "0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b", + "occurrences": [ + { + "topics": [ + "0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b", + "0x0000000000000000000000005fbdb2315678afecb367f032d93f642f64180aa3" + ], + "data": "0x" + } + ] + }, + { + "sig": "Approval(address,address,uint256)", + "topic0": "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925", + "occurrences": [] + }, + { + "sig": "Initialized(uint8)", + "topic0": "0x7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498", + "occurrences": [ + { + "topics": [ + "0x7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb3847402498" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000000001" + } + ] + }, + { + "sig": "Transfer(address,address,uint256)", + "topic0": "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "occurrences": [] + }, + { + "sig": "DummyEvent()", + "topic0": "0x321d83059d08b108790a2ca1fd77d843211cdfff5dc3b5d7fb4574be368d4497", + "occurrences": [ + { + "topics": [ + "0x321d83059d08b108790a2ca1fd77d843211cdfff5dc3b5d7fb4574be368d4497" + ], + "data": "0x" + } + ] + } + ], + "unvalidated_metadata": { + "author_name": "Author", + "description": "System Description", + "hardfork": [ + "paris", + "shanghai" + ], + "audit_report": "https://example.org/report.pdf", + "source_url": "https://github.com/source/code", + "security_contact": "security@example.org", + "implementation_name": "MyToken" + } +} \ No newline at end of file diff --git a/tests/test_end_to_end.rs b/tests/test_end_to_end.rs index 16bde97b..b219856a 100644 --- a/tests/test_end_to_end.rs +++ b/tests/test_end_to_end.rs @@ -649,13 +649,11 @@ mod tests { println!("{}", &String::from_utf8_lossy(&assert.get_output().stdout)); // Uncomment to regenerate expected files - /* - std::fs::copy( - outfile.path(), - Path::new("tests/expected_dvfs/MyToken.dvf.json"), - ) - .unwrap(); - */ + // std::fs::copy( + // outfile.path(), + // Path::new("tests/expected_dvfs/MyToken.dvf.json"), + // ) + // .unwrap(); assert_eq_files( &outfile.path(), @@ -714,13 +712,11 @@ mod tests { println!("{}", &String::from_utf8_lossy(&assert.get_output().stdout)); // Uncomment to regenerate expected files - /* - std::fs::copy( - proxy_outfile.path(), - Path::new("tests/expected_dvfs/TransparentUpgradeableProxy.dvf.json"), - ) - .unwrap(); - */ + // std::fs::copy( + // proxy_outfile.path(), + // Path::new("tests/expected_dvfs/TransparentUpgradeableProxy.dvf.json"), + // ) + // .unwrap(); // @note that this fails, since the wrong name is stored in the registry assert_eq_files( @@ -774,6 +770,183 @@ mod tests { } } + struct ContractVersion { + contract: String, + expected: String, + deployment_block: u64, + address: String, + } + + #[test] + fn test_e2e_proxy_upgrade() { + let port = 8548u16; + let config_file = match DVFConfig::test_config_file(Some(port)) { + Ok(config) => config, + Err(err) => { + println!("{}", err); + assert!(false); + return; + } + }; + let url = format!("http://localhost:{}", port).to_string(); + for client_type in LocalClientType::iterator() { + let local_client = start_local_client(client_type.clone(), port); + + // forge script script/Deploy_Proxy.s.sol --rpc-url "http://127.0.0.1:8546" --broadcast + let mut forge_cmd = Command::new("forge"); + forge_cmd.current_dir("tests/Contracts"); + let forge_assert = forge_cmd + .args(&[ + "script", + "script/Deploy_ProxyUpgrade.s.sol", + "--rpc-url", + &url, + "--broadcast", + "--slow", + ]) + .assert() + .success(); + println!( + "{}", + &String::from_utf8_lossy(&forge_assert.get_output().stdout) + ); + let v1: ContractVersion = ContractVersion { + contract: String::from("MyToken"), + expected: String::from("tests/expected_dvfs/MyTokenUpgrade.dvf.json"), + deployment_block: 3, + address: String::from("0x5fbdb2315678afecb367f032d93f642f64180aa3"), + }; + let v2: ContractVersion = ContractVersion { + contract: String::from("MyTokenV2"), + expected: String::from("tests/expected_dvfs/MyTokenUpgradeV2.dvf.json"), + deployment_block: 24, + address: String::from("0x4ed7c70f96b99c776995fb64377f0d4ab3b0e1c1"), + }; + + for v in [v1, v2] { + let outfile = NamedTempFile::new().unwrap(); + let mut dvf_cmd = Command::cargo_bin("dv").unwrap(); + let assert = dvf_cmd + .args(&[ + "--config", + &config_file.path().to_string_lossy(), + "init", + "--address", + &v.address, + "--chainid", + &chain_id_str(client_type.clone()), + "--project", + "tests/Contracts/", + "--initblock", + &v.deployment_block.to_string(), + "--contractname", + &v.contract, + &outfile.path().to_string_lossy(), + ]) + .assert() + .success(); + println!("{}", &String::from_utf8_lossy(&assert.get_output().stdout)); + + // Uncomment to regenerate expected files + // std::fs::copy(outfile.path(), &Path::new(&v.expected)).unwrap(); + + assert_eq_files(&outfile.path(), &Path::new(&v.expected)); + + // Sign + let mut dvf_cmd = Command::cargo_bin("dv").unwrap(); + dvf_cmd + .args(&[ + "--config", + &config_file.path().to_string_lossy(), + "sign", + &outfile.path().to_string_lossy(), + ]) + .assert() + .success(); + } + + let proxy_outfile = NamedTempFile::new().unwrap(); + let mut dvf_cmd = Command::cargo_bin("dv").unwrap(); + let assert = dvf_cmd + .args(&[ + "--config", + &config_file.path().to_string_lossy(), + "init", + "--address", + "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", + "--chainid", + &chain_id_str(client_type.clone()), + "--project", + "tests/Contracts/", + "--contractname", + "TransparentUpgradeableProxy", + "--implementation", + "MyToken", + "--initblock", + "3", + &proxy_outfile.path().to_string_lossy(), + ]) + .assert() + .success(); + println!("{}", &String::from_utf8_lossy(&assert.get_output().stdout)); + + // Uncomment to regenerate expected files + // std::fs::copy( + // proxy_outfile.path(), + // Path::new("tests/expected_dvfs/TransparentUpgradeableProxyUpgrade.dvf.json"), + // ) + // .unwrap(); + + // @note that this fails, since the wrong name is stored in the registry + assert_eq_files( + proxy_outfile.path(), + Path::new("tests/expected_dvfs/TransparentUpgradeableProxyUpgrade.dvf.json"), + ); + + // Update + let mut dvf_cmd: Command = Command::cargo_bin("dv").unwrap(); + let assert = dvf_cmd + .args(&[ + "--config", + &config_file.path().to_string_lossy(), + "update", + "--validationblock", + "26", + "--discover", + &proxy_outfile.path().to_string_lossy(), + "--implementation", + "MyTokenV2", + "--project", + "tests/Contracts/", + ]) + .assert() + .success(); + println!("{}", &String::from_utf8_lossy(&assert.get_output().stdout)); + + let updated_path = format!( + "{}_updated.dvf.json", + proxy_outfile.path().to_string_lossy() + ); + // Uncomment to regenerate expected files + // std::fs::copy( + // Path::new(&updated_path), + // Path::new( + // "tests/expected_dvfs/TransparentUpgradeableProxyUpgrade_updated.dvf.json", + // ), + // ) + // .unwrap(); + + assert_eq_files( + Path::new(&updated_path), + Path::new( + "tests/expected_dvfs/TransparentUpgradeableProxyUpgrade_updated.dvf.json", + ), + ); + + drop(local_client); + } + } + #[test] fn test_e2e_init_validate() { let port = 8549u16;