diff --git a/crates/abi/README.md b/crates/abi/README.md index 600524bdb7426..908d463f4a141 100644 --- a/crates/abi/README.md +++ b/crates/abi/README.md @@ -8,3 +8,11 @@ Additional bindings can be generated by doing the following: 2. update the [build script](./build.rs)'s `MultiAbigen::new` call; 3. build the crate once with `cargo build -p foundry-abi`, generating the bindings for the first time; 4. export the newly-generated bindings at the root of the crate, in [`lib.rs`](./src/lib.rs). + +New cheatcodes can be added by doing the following: + +1. add its Solidity definition(s) in [`HEVM.sol`](./abi/HEVM.sol), bindings should regenerate automatically; +2. implement it in [`foundry-evm`](../evm/src/executor/inspector/cheatcodes/); +3. update the [`Vm.sol`](../../testdata/cheats/Vm.sol) test interface; +4. add tests in [`testdata`](../../testdata/cheats/); +5. open a PR to [`forge-std`](https://github.com/foundry-rs/forge-std) to add it to the `Vm` interface. diff --git a/crates/abi/abi/HEVM.sol b/crates/abi/abi/HEVM.sol index 8dda26c9e44bf..0528b17462714 100644 --- a/crates/abi/abi/HEVM.sol +++ b/crates/abi/abi/HEVM.sol @@ -119,13 +119,14 @@ startBroadcast(uint256) stopBroadcast() projectRoot()(string) +openFile(string) readFile(string)(string) readFileBinary(string)(bytes) +readLine(string)(string) writeFile(string,string) writeFileBinary(string,bytes) -openFile(string) -readLine(string)(string) writeLine(string,string) +copyFile(string,string) closeFile(string) removeFile(string) createDir(string, bool) diff --git a/crates/abi/src/bindings/hevm.rs b/crates/abi/src/bindings/hevm.rs index 669b9488fb312..c098519faf3b8 100644 --- a/crates/abi/src/bindings/hevm.rs +++ b/crates/abi/src/bindings/hevm.rs @@ -270,6 +270,29 @@ pub mod hevm { }, ], ), + ( + ::std::borrow::ToOwned::to_owned("copyFile"), + ::std::vec![ + ::ethers_core::abi::ethabi::Function { + name: ::std::borrow::ToOwned::to_owned("copyFile"), + inputs: ::std::vec![ + ::ethers_core::abi::ethabi::Param { + name: ::std::string::String::new(), + kind: ::ethers_core::abi::ethabi::ParamType::String, + internal_type: ::core::option::Option::None, + }, + ::ethers_core::abi::ethabi::Param { + name: ::std::string::String::new(), + kind: ::ethers_core::abi::ethabi::ParamType::String, + internal_type: ::core::option::Option::None, + }, + ], + outputs: ::std::vec![], + constant: ::core::option::Option::None, + state_mutability: ::ethers_core::abi::ethabi::StateMutability::NonPayable, + }, + ], + ), ( ::std::borrow::ToOwned::to_owned("createDir"), ::std::vec![ @@ -5018,6 +5041,16 @@ pub mod hevm { .method_hash([255, 72, 60, 84], p0) .expect("method not found (this should never happen)") } + ///Calls the contract's `copyFile` (0xa54a87d8) function + pub fn copy_file( + &self, + p0: ::std::string::String, + p1: ::std::string::String, + ) -> ::ethers_contract::builders::ContractCall { + self.0 + .method_hash([165, 74, 135, 216], (p0, p1)) + .expect("method not found (this should never happen)") + } ///Calls the contract's `createDir` (0x168b64d3) function pub fn create_dir( &self, @@ -7102,6 +7135,19 @@ pub mod hevm { )] #[ethcall(name = "coinbase", abi = "coinbase(address)")] pub struct CoinbaseCall(pub ::ethers_core::types::Address); + ///Container type for all input parameters for the `copyFile` function with signature `copyFile(string,string)` and selector `0xa54a87d8` + #[derive( + Clone, + ::ethers_contract::EthCall, + ::ethers_contract::EthDisplay, + Default, + Debug, + PartialEq, + Eq, + Hash + )] + #[ethcall(name = "copyFile", abi = "copyFile(string,string)")] + pub struct CopyFileCall(pub ::std::string::String, pub ::std::string::String); ///Container type for all input parameters for the `createDir` function with signature `createDir(string,bool)` and selector `0x168b64d3` #[derive( Clone, @@ -9781,6 +9827,7 @@ pub mod hevm { ClearMockedCalls(ClearMockedCallsCall), CloseFile(CloseFileCall), Coinbase(CoinbaseCall), + CopyFile(CopyFileCall), CreateDir(CreateDirCall), CreateFork1(CreateFork1Call), CreateFork2(CreateFork2Call), @@ -10027,6 +10074,10 @@ pub mod hevm { = ::decode(data) { return Ok(Self::Coinbase(decoded)); } + if let Ok(decoded) + = ::decode(data) { + return Ok(Self::CopyFile(decoded)); + } if let Ok(decoded) = ::decode(data) { return Ok(Self::CreateDir(decoded)); @@ -10847,6 +10898,7 @@ pub mod hevm { ::ethers_core::abi::AbiEncode::encode(element) } Self::Coinbase(element) => ::ethers_core::abi::AbiEncode::encode(element), + Self::CopyFile(element) => ::ethers_core::abi::AbiEncode::encode(element), Self::CreateDir(element) => { ::ethers_core::abi::AbiEncode::encode(element) } @@ -11315,6 +11367,7 @@ pub mod hevm { Self::ClearMockedCalls(element) => ::core::fmt::Display::fmt(element, f), Self::CloseFile(element) => ::core::fmt::Display::fmt(element, f), Self::Coinbase(element) => ::core::fmt::Display::fmt(element, f), + Self::CopyFile(element) => ::core::fmt::Display::fmt(element, f), Self::CreateDir(element) => ::core::fmt::Display::fmt(element, f), Self::CreateFork1(element) => ::core::fmt::Display::fmt(element, f), Self::CreateFork2(element) => ::core::fmt::Display::fmt(element, f), @@ -11592,6 +11645,11 @@ pub mod hevm { Self::Coinbase(value) } } + impl ::core::convert::From for HEVMCalls { + fn from(value: CopyFileCall) -> Self { + Self::CopyFile(value) + } + } impl ::core::convert::From for HEVMCalls { fn from(value: CreateDirCall) -> Self { Self::CreateDir(value) diff --git a/crates/cli/src/forge/cmd/script/multi.rs b/crates/cli/src/forge/cmd/script/multi.rs index 566c31e875e9c..5e597533ce9b2 100644 --- a/crates/cli/src/forge/cmd/script/multi.rs +++ b/crates/cli/src/forge/cmd/script/multi.rs @@ -9,15 +9,15 @@ use ethers::{ signers::LocalWallet, }; use eyre::{ContextCompat, WrapErr}; +use foundry_cli::utils::now; use foundry_common::{fs, get_http_provider}; use foundry_config::Config; use futures::future::join_all; use serde::{Deserialize, Serialize}; use std::{ - io::BufWriter, + io::{BufWriter, Write}, path::{Path, PathBuf}, sync::Arc, - time::{SystemTime, UNIX_EPOCH}, }; use tracing::log::trace; @@ -31,11 +31,8 @@ pub struct MultiChainSequence { impl Drop for MultiChainSequence { fn drop(&mut self) { - self.deployments.iter_mut().for_each(|sequence| { - sequence.sort_receipts(); - }); - - self.save().expect("not able to save multi deployment sequence"); + self.deployments.iter_mut().for_each(|sequence| sequence.sort_receipts()); + self.save().expect("could not save multi deployment sequence"); } } @@ -50,14 +47,7 @@ impl MultiChainSequence { let path = MultiChainSequence::get_path(&log_folder.join("multi"), sig, target, broadcasted)?; - Ok(MultiChainSequence { - deployments, - path, - timestamp: SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Wrong system time.") - .as_secs(), - }) + Ok(MultiChainSequence { deployments, path, timestamp: now().as_secs() }) } /// Saves to ./broadcast/multi/contract_filename[-timestamp]/sig.json @@ -85,8 +75,7 @@ impl MultiChainSequence { let filename = sig .split_once('(') .wrap_err_with(|| format!("Failed to compute file name: Signature {sig} is invalid."))? - .0 - .to_string(); + .0; out.push(format!("{filename}.json")); Ok(out) @@ -100,20 +89,20 @@ impl MultiChainSequence { /// Saves the transactions as file if it's a standalone deployment. pub fn save(&mut self) -> eyre::Result<()> { - self.timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); - let path = self.path.to_string_lossy(); + self.timestamp = now().as_secs(); //../Contract-latest/run.json - serde_json::to_writer_pretty(BufWriter::new(fs::create_file(&self.path)?), &self)?; + let mut writer = BufWriter::new(fs::create_file(&self.path)?); + serde_json::to_writer_pretty(&mut writer, &self)?; + writer.flush()?; //../Contract-[timestamp]/run.json + let path = self.path.to_string_lossy(); let file = PathBuf::from(&path.replace("-latest", &format!("-{}", self.timestamp))); + fs::create_dir_all(file.parent().unwrap())?; + fs::copy(&self.path, &file)?; - fs::create_dir_all(file.parent().expect("to have a file."))?; - - serde_json::to_writer_pretty(BufWriter::new(fs::create_file(file)?), &self)?; - - println!("\nTransactions saved to: {path}\n"); + println!("\nTransactions saved to: {}\n", self.path.display()); Ok(()) } diff --git a/crates/cli/src/forge/cmd/script/sequence.rs b/crates/cli/src/forge/cmd/script/sequence.rs index 1abe2352c2de1..7098cafcc6168 100644 --- a/crates/cli/src/forge/cmd/script/sequence.rs +++ b/crates/cli/src/forge/cmd/script/sequence.rs @@ -13,14 +13,14 @@ use ethers::{ types::transaction::eip2718::TypedTransaction, }; use eyre::{ContextCompat, WrapErr}; +use foundry_cli::utils::now; use foundry_common::{fs, shell, SELECTOR_LEN}; use foundry_config::Config; use serde::{Deserialize, Serialize}; use std::{ collections::{HashMap, VecDeque}, - io::BufWriter, + io::{BufWriter, Write}, path::{Path, PathBuf}, - time::{SystemTime, UNIX_EPOCH}, }; use tracing::trace; use yansi::Paint; @@ -102,10 +102,7 @@ impl ScriptSequence { pending: vec![], path, sensitive_path, - timestamp: SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Wrong system time.") - .as_secs(), + timestamp: now().as_secs(), libraries: vec![], chain, multi: is_multi, @@ -152,43 +149,34 @@ impl ScriptSequence { /// Saves the transactions as file if it's a standalone deployment. pub fn save(&mut self) -> eyre::Result<()> { - if !self.multi && !self.transactions.is_empty() { - self.timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); - - let sensitive_script_sequence: SensitiveScriptSequence = self.into(); - - let path = self.path.to_string_lossy(); - let sensitive_path = self.sensitive_path.to_string_lossy(); - - // broadcast folder writes - //../run-latest.json - serde_json::to_writer_pretty(BufWriter::new(fs::create_file(&self.path)?), &self)?; - //../run-[timestamp].json - serde_json::to_writer_pretty( - BufWriter::new(fs::create_file( - path.replace("latest.json", &format!("{}.json", self.timestamp)), - )?), - &self, - )?; - - // cache folder writes - //../run-latest.json - serde_json::to_writer_pretty( - BufWriter::new(fs::create_file(&self.sensitive_path)?), - &sensitive_script_sequence, - )?; - //../run-[timestamp].json - serde_json::to_writer_pretty( - BufWriter::new(fs::create_file( - sensitive_path.replace("latest.json", &format!("{}.json", self.timestamp)), - )?), - &sensitive_script_sequence, - )?; - - shell::println(format!("\nTransactions saved to: {path}\n"))?; - shell::println(format!("Sensitive values saved to: {sensitive_path}\n"))?; + if self.multi || self.transactions.is_empty() { + return Ok(()) } + self.timestamp = now().as_secs(); + let ts_name = format!("run-{}.json", self.timestamp); + + let sensitive_script_sequence: SensitiveScriptSequence = self.into(); + + // broadcast folder writes + //../run-latest.json + let mut writer = BufWriter::new(fs::create_file(&self.path)?); + serde_json::to_writer_pretty(&mut writer, &self)?; + writer.flush()?; + //../run-[timestamp].json + fs::copy(&self.path, self.path.with_file_name(&ts_name))?; + + // cache folder writes + //../run-latest.json + let mut writer = BufWriter::new(fs::create_file(&self.sensitive_path)?); + serde_json::to_writer_pretty(&mut writer, &sensitive_script_sequence)?; + writer.flush()?; + //../run-[timestamp].json + fs::copy(&self.sensitive_path, self.sensitive_path.with_file_name(&ts_name))?; + + shell::println(format!("\nTransactions saved to: {}\n", self.path.display()))?; + shell::println(format!("Sensitive values saved to: {}\n", self.sensitive_path.display()))?; + Ok(()) } diff --git a/crates/cli/src/utils/mod.rs b/crates/cli/src/utils/mod.rs index d304e80e86071..f60d1d924d158 100644 --- a/crates/cli/src/utils/mod.rs +++ b/crates/cli/src/utils/mod.rs @@ -14,7 +14,7 @@ use std::{ path::{Path, PathBuf}, process::{Command, Output, Stdio}, str::FromStr, - time::Duration, + time::{Duration, SystemTime, UNIX_EPOCH}, }; use tracing_error::ErrorLayer; use tracing_subscriber::prelude::*; @@ -153,6 +153,11 @@ pub fn parse_delay(delay: &str) -> Result { Ok(delay) } +/// Returns the current time as a [`Duration`] since the Unix epoch. +pub fn now() -> Duration { + SystemTime::now().duration_since(UNIX_EPOCH).expect("time went backwards") +} + /// Runs the `future` in a new [`tokio::runtime::Runtime`] #[allow(unused)] pub fn block_on(future: F) -> F::Output { diff --git a/crates/common/src/errors/fs.rs b/crates/common/src/errors/fs.rs index 4f7afc9b6496a..b929e7838140e 100644 --- a/crates/common/src/errors/fs.rs +++ b/crates/common/src/errors/fs.rs @@ -14,6 +14,9 @@ pub enum FsPathError { /// Provides additional path context for [`std::fs::read`]. #[error("failed to read from {path:?}: {source}")] Read { source: io::Error, path: PathBuf }, + /// Provides additional path context for [`std::fs::copy`]. + #[error("failed to copy from {from:?} to {to:?}: {source}")] + Copy { source: io::Error, from: PathBuf, to: PathBuf }, /// Provides additional path context for [`std::fs::read_link`]. #[error("failed to read from {path:?}: {source}")] ReadLink { source: io::Error, path: PathBuf }, @@ -51,6 +54,11 @@ impl FsPathError { FsPathError::Read { source, path: path.into() } } + /// Returns the complementary error variant for [`std::fs::copy`]. + pub fn copy(source: io::Error, from: impl Into, to: impl Into) -> Self { + FsPathError::Copy { source, from: from.into(), to: to.into() } + } + /// Returns the complementary error variant for [`std::fs::read_link`]. pub fn read_link(source: io::Error, path: impl Into) -> Self { FsPathError::ReadLink { source, path: path.into() } @@ -85,33 +93,37 @@ impl FsPathError { impl AsRef for FsPathError { fn as_ref(&self) -> &Path { match self { - FsPathError::Write { path, .. } => path, - FsPathError::Read { path, .. } => path, - FsPathError::ReadLink { path, .. } => path, - FsPathError::CreateDir { path, .. } => path, - FsPathError::RemoveDir { path, .. } => path, - FsPathError::CreateFile { path, .. } => path, - FsPathError::RemoveFile { path, .. } => path, - FsPathError::Open { path, .. } => path, - FsPathError::ReadJson { path, .. } => path, - FsPathError::WriteJson { path, .. } => path, + Self::Write { path, .. } | + Self::Read { path, .. } | + Self::ReadLink { path, .. } | + Self::Copy { from: path, .. } | + Self::CreateDir { path, .. } | + Self::RemoveDir { path, .. } | + Self::CreateFile { path, .. } | + Self::RemoveFile { path, .. } | + Self::Open { path, .. } | + Self::ReadJson { path, .. } | + Self::WriteJson { path, .. } => path, } } } impl From for io::Error { - fn from(err: FsPathError) -> Self { - match err { - FsPathError::Write { source, .. } => source, - FsPathError::Read { source, .. } => source, - FsPathError::ReadLink { source, .. } => source, - FsPathError::CreateDir { source, .. } => source, - FsPathError::RemoveDir { source, .. } => source, - FsPathError::CreateFile { source, .. } => source, - FsPathError::RemoveFile { source, .. } => source, + fn from(value: FsPathError) -> Self { + match value { + FsPathError::Write { source, .. } | + FsPathError::Read { source, .. } | + FsPathError::ReadLink { source, .. } | + FsPathError::Copy { source, .. } | + FsPathError::CreateDir { source, .. } | + FsPathError::RemoveDir { source, .. } | + FsPathError::CreateFile { source, .. } | + FsPathError::RemoveFile { source, .. } | FsPathError::Open { source, .. } => source, - FsPathError::ReadJson { source, .. } => source.into(), - FsPathError::WriteJson { source, .. } => source.into(), + + FsPathError::ReadJson { source, .. } | FsPathError::WriteJson { source, .. } => { + source.into() + } } } } diff --git a/crates/common/src/fs.rs b/crates/common/src/fs.rs index 119b07653e7ab..25c20ab49f50a 100644 --- a/crates/common/src/fs.rs +++ b/crates/common/src/fs.rs @@ -3,6 +3,7 @@ use crate::errors::FsPathError; use serde::{de::DeserializeOwned, Serialize}; use std::{ fs::{self, File}, + io::{BufWriter, Write}, path::{Component, Path, PathBuf}, }; @@ -43,15 +44,16 @@ pub fn read_json_file(path: &Path) -> Result { // https://github.com/serde-rs/json/issues/160 let bytes = read(path)?; serde_json::from_slice(&bytes) - .map_err(|source| FsPathError::ReadJson { source, path: path.to_path_buf() }) + .map_err(|source| FsPathError::ReadJson { source, path: path.into() }) } /// Writes the object as a JSON object. pub fn write_json_file(path: &Path, obj: &T) -> Result<()> { let file = create_file(path)?; - let file = std::io::BufWriter::new(file); - serde_json::to_writer(file, obj) - .map_err(|source| FsPathError::WriteJson { source, path: path.to_path_buf() }) + let mut writer = BufWriter::new(file); + serde_json::to_writer(&mut writer, obj) + .map_err(|source| FsPathError::WriteJson { source, path: path.into() })?; + writer.flush().map_err(|e| FsPathError::write(e, path)) } /// Wrapper for `std::fs::write` @@ -60,6 +62,13 @@ pub fn write(path: impl AsRef, contents: impl AsRef<[u8]>) -> Result<()> { fs::write(path, contents).map_err(|err| FsPathError::write(err, path)) } +/// Wrapper for `std::fs::copy` +pub fn copy(from: impl AsRef, to: impl AsRef) -> Result { + let from = from.as_ref(); + let to = to.as_ref(); + fs::copy(from, to).map_err(|err| FsPathError::copy(err, from, to)) +} + /// Wrapper for `std::fs::create_dir` pub fn create_dir(path: impl AsRef) -> Result<()> { let path = path.as_ref(); diff --git a/crates/common/src/selectors.rs b/crates/common/src/selectors.rs index a2e6e159f752c..e87c55148b5e0 100644 --- a/crates/common/src/selectors.rs +++ b/crates/common/src/selectors.rs @@ -190,7 +190,8 @@ impl SignEthClient { .get(selector) .ok_or(eyre::eyre!("No signature found"))? .iter() - .filter_map(|d| (!d.filtered).then(|| d.name.clone())) + .filter(|&d| !d.filtered) + .map(|d| d.name.clone()) .collect::>()) } diff --git a/crates/evm/src/executor/fork/cache.rs b/crates/evm/src/executor/fork/cache.rs index 21adb59e02a10..dbd4cfe747c8e 100644 --- a/crates/evm/src/executor/fork/cache.rs +++ b/crates/evm/src/executor/fork/cache.rs @@ -7,7 +7,13 @@ use revm::{ DatabaseCommit, }; use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer}; -use std::{collections::BTreeSet, fs, io::BufWriter, path::PathBuf, sync::Arc}; +use std::{ + collections::BTreeSet, + fs, + io::{BufWriter, Write}, + path::PathBuf, + sync::Arc, +}; use url::Url; @@ -351,22 +357,30 @@ impl JsonBlockCacheDB { self.cache_path.is_none() } - /// Flushes the DB to disk if caching is enabled + /// Flushes the DB to disk if caching is enabled. + #[tracing::instrument(level = "warn", skip_all, fields(path = ?self.cache_path))] pub fn flush(&self) { - // writes the data to a json file - if let Some(ref path) = self.cache_path { - trace!(target: "cache", "saving json cache path={:?}", path); - if let Some(parent) = path.parent() { - let _ = fs::create_dir_all(parent); - } - let _ = fs::File::create(path) - .map_err(|e| warn!(target: "cache", "Failed to open json cache for writing: {}", e)) - .and_then(|f| { - serde_json::to_writer(BufWriter::new(f), &self.data) - .map_err(|e| warn!(target: "cache" ,"Failed to write to json cache: {}", e)) - }); - trace!(target: "cache", "saved json cache path={:?}", path); + let Some(path) = &self.cache_path else { return }; + trace!(target: "cache", "saving json cache"); + + if let Some(parent) = path.parent() { + let _ = fs::create_dir_all(parent); + } + + let file = match fs::File::create(path) { + Ok(file) => file, + Err(e) => return warn!(target: "cache", %e, "Failed to open json cache for writing"), + }; + + let mut writer = BufWriter::new(file); + if let Err(e) = serde_json::to_writer(&mut writer, &self.data) { + return warn!(target: "cache", %e, "Failed to write to json cache") } + if let Err(e) = writer.flush() { + return warn!(target: "cache", %e, "Failed to flush to json cache") + } + + trace!(target: "cache", "saved json cache"); } } diff --git a/crates/evm/src/executor/inspector/cheatcodes/env.rs b/crates/evm/src/executor/inspector/cheatcodes/env.rs index 17cd37e7e9b0a..27fa5bdd390a0 100644 --- a/crates/evm/src/executor/inspector/cheatcodes/env.rs +++ b/crates/evm/src/executor/inspector/cheatcodes/env.rs @@ -333,7 +333,9 @@ pub fn apply( HEVMCalls::Difficulty(inner) => { ensure!( data.env.cfg.spec_id < SpecId::MERGE, - "Difficulty is not supported after the Paris hard fork. Please use vm.prevrandao instead. For more information, please see https://eips.ethereum.org/EIPS/eip-4399" + "`difficulty` is not supported after the Paris hard fork, \ + use `prevrandao` instead. \ + For more information, please see https://eips.ethereum.org/EIPS/eip-4399" ); data.env.block.difficulty = inner.0.into(); Bytes::new() @@ -341,7 +343,9 @@ pub fn apply( HEVMCalls::Prevrandao(inner) => { ensure!( data.env.cfg.spec_id >= SpecId::MERGE, - "Prevrandao is not supported before the Paris hard fork. Please use vm.difficulty instead. For more information, please see https://eips.ethereum.org/EIPS/eip-4399" + "`prevrandao` is not supported before the Paris hard fork, \ + use `difficulty` instead. \ + For more information, please see https://eips.ethereum.org/EIPS/eip-4399" ); data.env.block.prevrandao = Some(B256::from(inner.0)); Bytes::new() diff --git a/crates/evm/src/executor/inspector/cheatcodes/fs.rs b/crates/evm/src/executor/inspector/cheatcodes/fs.rs index 184428c511910..e6616c30341fa 100644 --- a/crates/evm/src/executor/inspector/cheatcodes/fs.rs +++ b/crates/evm/src/executor/inspector/cheatcodes/fs.rs @@ -93,6 +93,15 @@ fn write_line(state: &Cheatcodes, path: impl AsRef, line: &str) -> Result Ok(Bytes::new()) } +fn copy_file(state: &Cheatcodes, from: impl AsRef, to: impl AsRef) -> Result { + let from = state.config.ensure_path_allowed(from, FsAccessKind::Read)?; + let to = state.config.ensure_path_allowed(to, FsAccessKind::Write)?; + state.config.ensure_not_foundry_toml(&to)?; + + let n = fs::copy(from, to)?; + Ok(abi::encode(&[Token::Uint(n.into())]).into()) +} + fn close_file(state: &mut Cheatcodes, path: impl AsRef) -> Result { let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?; @@ -251,6 +260,7 @@ pub fn apply(state: &mut Cheatcodes, call: &HEVMCalls) -> Option { HEVMCalls::WriteFile(inner) => write_file(state, &inner.0, &inner.1), HEVMCalls::WriteFileBinary(inner) => write_file(state, &inner.0, &inner.1), HEVMCalls::WriteLine(inner) => write_line(state, &inner.0, &inner.1), + HEVMCalls::CopyFile(inner) => copy_file(state, &inner.0, &inner.1), HEVMCalls::CloseFile(inner) => close_file(state, &inner.0), HEVMCalls::RemoveFile(inner) => remove_file(state, &inner.0), HEVMCalls::FsMetadata(inner) => fs_metadata(state, &inner.0), diff --git a/testdata/cheats/Fs.t.sol b/testdata/cheats/Fs.t.sol index 482c6b871dabc..81aec047f7080 100644 --- a/testdata/cheats/Fs.t.sol +++ b/testdata/cheats/Fs.t.sol @@ -111,6 +111,15 @@ contract FsTest is DSTest { fsProxy.writeFileBinary("/etc/hosts", "malicious stuff"); } + function testCopyFile() public { + string memory from = "fixtures/File/read.txt"; + string memory to = "fixtures/File/copy.txt"; + uint64 copied = vm.copyFile(from, to); + assertEq(vm.fsMetadata(to).length, uint256(copied)); + assertEq(vm.readFile(from), vm.readFile(to)); + vm.removeFile(to); + } + function testWriteLine() public { fsProxy = new FsProxy(); diff --git a/testdata/cheats/Vm.sol b/testdata/cheats/Vm.sol index 35bb56b459a41..eda063914a21a 100644 --- a/testdata/cheats/Vm.sol +++ b/testdata/cheats/Vm.sol @@ -356,6 +356,12 @@ interface Vm { // (path, data) => () function writeLine(string calldata, string calldata) external; + // Copies the contents of one file to another. This function will **overwrite** the contents of `to`. + // On success, the total number of bytes copied is returned and it is equal to the length of the `to` file as reported by `metadata`. + // Both `from` and `to` are relative to the project root. + // (from, to) => (copied) + function copyFile(string calldata, string calldata) external returns (uint64); + // Closes file for reading, resetting the offset and allowing to read it from beginning with readLine. // `path` is relative to the project root. // (path) => ()