diff --git a/crates/abi/abi/HEVM.sol b/crates/abi/abi/HEVM.sol index 008dd1cbc084..f55d826ee39a 100644 --- a/crates/abi/abi/HEVM.sol +++ b/crates/abi/abi/HEVM.sol @@ -5,7 +5,7 @@ struct DirEntry { string errorMessage; string path; uint64 depth; bool isDir; bo struct FsMetadata { bool isDir; bool isSymlink; uint256 length; bool readOnly; uint256 modified; uint256 accessed; uint256 created; } struct Wallet { address addr; uint256 publicKeyX; uint256 publicKeyY; uint256 privateKey; } struct FfiResult { int32 exitCode; bytes stdout; bytes stderr; } -struct AccountAccess { address account; uint8 kind; bool initialized; uint256 value; bytes data; bool reverted; } +struct AccountAccess { address accessor; address account; uint256 kind; bool initialized; uint256 oldBalance; uint256 newBalance; bytes deployedCode; uint256 value; bytes data; bool reverted; StorageAccess[] storageAccesses; } struct StorageAccess { address account; bytes32 slot; bool isWrite; bytes32 previousValue; bytes32 newValue; bool reverted; } allowCheatcodes(address) @@ -86,11 +86,8 @@ record() accesses(address)(bytes32[], bytes32[]) skip(bool) -recordAccountAccesses() -getRecordedAccountAccesses()(AccountAccess[]) - -recordStorageAccesses() -getRecordedStorageAccesses()(StorageAccess[]) +recordStateDiff() +getStateDiff()(AccountAccess[]) recordLogs() getRecordedLogs()(Log[]) diff --git a/crates/abi/src/bindings/hevm.rs b/crates/abi/src/bindings/hevm.rs index 8bcb7ee6455a..43be4533d78d 100644 --- a/crates/abi/src/bindings/hevm.rs +++ b/crates/abi/src/bindings/hevm.rs @@ -2308,39 +2308,6 @@ pub mod hevm { }, ], ), - ( - ::std::borrow::ToOwned::to_owned("getRecordedAccountAccesses"), - ::std::vec![ - ::ethers_core::abi::ethabi::Function { - name: ::std::borrow::ToOwned::to_owned( - "getRecordedAccountAccesses", - ), - inputs: ::std::vec![], - outputs: ::std::vec![ - ::ethers_core::abi::ethabi::Param { - name: ::std::string::String::new(), - kind: ::ethers_core::abi::ethabi::ParamType::Array( - ::std::boxed::Box::new( - ::ethers_core::abi::ethabi::ParamType::Tuple( - ::std::vec![ - ::ethers_core::abi::ethabi::ParamType::Address, - ::ethers_core::abi::ethabi::ParamType::Uint(8usize), - ::ethers_core::abi::ethabi::ParamType::Bool, - ::ethers_core::abi::ethabi::ParamType::Uint(256usize), - ::ethers_core::abi::ethabi::ParamType::Bytes, - ::ethers_core::abi::ethabi::ParamType::Bool, - ], - ), - ), - ), - internal_type: ::core::option::Option::None, - }, - ], - constant: ::core::option::Option::None, - state_mutability: ::ethers_core::abi::ethabi::StateMutability::NonPayable, - }, - ], - ), ( ::std::borrow::ToOwned::to_owned("getRecordedLogs"), ::std::vec![ @@ -2373,12 +2340,10 @@ pub mod hevm { ], ), ( - ::std::borrow::ToOwned::to_owned("getRecordedStorageAccesses"), + ::std::borrow::ToOwned::to_owned("getStateDiff"), ::std::vec![ ::ethers_core::abi::ethabi::Function { - name: ::std::borrow::ToOwned::to_owned( - "getRecordedStorageAccesses", - ), + name: ::std::borrow::ToOwned::to_owned("getStateDiff"), inputs: ::std::vec![], outputs: ::std::vec![ ::ethers_core::abi::ethabi::Param { @@ -2388,11 +2353,29 @@ pub mod hevm { ::ethers_core::abi::ethabi::ParamType::Tuple( ::std::vec![ ::ethers_core::abi::ethabi::ParamType::Address, - ::ethers_core::abi::ethabi::ParamType::FixedBytes(32usize), + ::ethers_core::abi::ethabi::ParamType::Address, + ::ethers_core::abi::ethabi::ParamType::Uint(256usize), ::ethers_core::abi::ethabi::ParamType::Bool, - ::ethers_core::abi::ethabi::ParamType::FixedBytes(32usize), - ::ethers_core::abi::ethabi::ParamType::FixedBytes(32usize), + ::ethers_core::abi::ethabi::ParamType::Uint(256usize), + ::ethers_core::abi::ethabi::ParamType::Uint(256usize), + ::ethers_core::abi::ethabi::ParamType::Bytes, + ::ethers_core::abi::ethabi::ParamType::Uint(256usize), + ::ethers_core::abi::ethabi::ParamType::Bytes, ::ethers_core::abi::ethabi::ParamType::Bool, + ::ethers_core::abi::ethabi::ParamType::Array( + ::std::boxed::Box::new( + ::ethers_core::abi::ethabi::ParamType::Tuple( + ::std::vec![ + ::ethers_core::abi::ethabi::ParamType::Address, + ::ethers_core::abi::ethabi::ParamType::FixedBytes(32usize), + ::ethers_core::abi::ethabi::ParamType::Bool, + ::ethers_core::abi::ethabi::ParamType::FixedBytes(32usize), + ::ethers_core::abi::ethabi::ParamType::FixedBytes(32usize), + ::ethers_core::abi::ethabi::ParamType::Bool, + ], + ), + ), + ), ], ), ), @@ -3774,20 +3757,6 @@ pub mod hevm { }, ], ), - ( - ::std::borrow::ToOwned::to_owned("recordAccountAccesses"), - ::std::vec![ - ::ethers_core::abi::ethabi::Function { - name: ::std::borrow::ToOwned::to_owned( - "recordAccountAccesses", - ), - inputs: ::std::vec![], - outputs: ::std::vec![], - constant: ::core::option::Option::None, - state_mutability: ::ethers_core::abi::ethabi::StateMutability::NonPayable, - }, - ], - ), ( ::std::borrow::ToOwned::to_owned("recordLogs"), ::std::vec![ @@ -3801,12 +3770,10 @@ pub mod hevm { ], ), ( - ::std::borrow::ToOwned::to_owned("recordStorageAccesses"), + ::std::borrow::ToOwned::to_owned("recordStateDiff"), ::std::vec![ ::ethers_core::abi::ethabi::Function { - name: ::std::borrow::ToOwned::to_owned( - "recordStorageAccesses", - ), + name: ::std::borrow::ToOwned::to_owned("recordStateDiff"), inputs: ::std::vec![], outputs: ::std::vec![], constant: ::core::option::Option::None, @@ -6417,26 +6384,6 @@ pub mod hevm { .method_hash([45, 3, 53, 171], p0) .expect("method not found (this should never happen)") } - ///Calls the contract's `getRecordedAccountAccesses` (0x4452c985) function - pub fn get_recorded_account_accesses( - &self, - ) -> ::ethers_contract::builders::ContractCall< - M, - ::std::vec::Vec< - ( - ::ethers_core::types::Address, - u8, - bool, - ::ethers_core::types::U256, - ::ethers_core::types::Bytes, - bool, - ), - >, - > { - self.0 - .method_hash([68, 82, 201, 133], ()) - .expect("method not found (this should never happen)") - } ///Calls the contract's `getRecordedLogs` (0x191553a4) function pub fn get_recorded_logs( &self, @@ -6448,17 +6395,38 @@ pub mod hevm { .method_hash([25, 21, 83, 164], ()) .expect("method not found (this should never happen)") } - ///Calls the contract's `getRecordedStorageAccesses` (0x03f1500a) function - pub fn get_recorded_storage_accesses( + ///Calls the contract's `getStateDiff` (0x80df01cc) function + pub fn get_state_diff( &self, ) -> ::ethers_contract::builders::ContractCall< M, ::std::vec::Vec< - (::ethers_core::types::Address, [u8; 32], bool, [u8; 32], [u8; 32], bool), + ( + ::ethers_core::types::Address, + ::ethers_core::types::Address, + ::ethers_core::types::U256, + bool, + ::ethers_core::types::U256, + ::ethers_core::types::U256, + ::ethers_core::types::Bytes, + ::ethers_core::types::U256, + ::ethers_core::types::Bytes, + bool, + ::std::vec::Vec< + ( + ::ethers_core::types::Address, + [u8; 32], + bool, + [u8; 32], + [u8; 32], + bool, + ), + >, + ), >, > { self.0 - .method_hash([3, 241, 80, 10], ()) + .method_hash([128, 223, 1, 204], ()) .expect("method not found (this should never happen)") } ///Calls the contract's `isDir` (0x7d15d019) function @@ -7005,26 +6973,18 @@ pub mod hevm { .method_hash([38, 108, 241, 9], ()) .expect("method not found (this should never happen)") } - ///Calls the contract's `recordAccountAccesses` (0xd9391303) function - pub fn record_account_accesses( - &self, - ) -> ::ethers_contract::builders::ContractCall { - self.0 - .method_hash([217, 57, 19, 3], ()) - .expect("method not found (this should never happen)") - } ///Calls the contract's `recordLogs` (0x41af2f52) function pub fn record_logs(&self) -> ::ethers_contract::builders::ContractCall { self.0 .method_hash([65, 175, 47, 82], ()) .expect("method not found (this should never happen)") } - ///Calls the contract's `recordStorageAccesses` (0x71009b87) function - pub fn record_storage_accesses( + ///Calls the contract's `recordStateDiff` (0xc9140b4b) function + pub fn record_state_diff( &self, ) -> ::ethers_contract::builders::ContractCall { self.0 - .method_hash([113, 0, 155, 135], ()) + .method_hash([201, 20, 11, 75], ()) .expect("method not found (this should never happen)") } ///Calls the contract's `rememberKey` (0x22100064) function @@ -9010,19 +8970,6 @@ pub mod hevm { )] #[ethcall(name = "getNonce", abi = "getNonce(address)")] pub struct GetNonce1Call(pub ::ethers_core::types::Address); - ///Container type for all input parameters for the `getRecordedAccountAccesses` function with signature `getRecordedAccountAccesses()` and selector `0x4452c985` - #[derive( - Clone, - ::ethers_contract::EthCall, - ::ethers_contract::EthDisplay, - Default, - Debug, - PartialEq, - Eq, - Hash - )] - #[ethcall(name = "getRecordedAccountAccesses", abi = "getRecordedAccountAccesses()")] - pub struct GetRecordedAccountAccessesCall; ///Container type for all input parameters for the `getRecordedLogs` function with signature `getRecordedLogs()` and selector `0x191553a4` #[derive( Clone, @@ -9036,7 +8983,7 @@ pub mod hevm { )] #[ethcall(name = "getRecordedLogs", abi = "getRecordedLogs()")] pub struct GetRecordedLogsCall; - ///Container type for all input parameters for the `getRecordedStorageAccesses` function with signature `getRecordedStorageAccesses()` and selector `0x03f1500a` + ///Container type for all input parameters for the `getStateDiff` function with signature `getStateDiff()` and selector `0x80df01cc` #[derive( Clone, ::ethers_contract::EthCall, @@ -9047,8 +8994,8 @@ pub mod hevm { Eq, Hash )] - #[ethcall(name = "getRecordedStorageAccesses", abi = "getRecordedStorageAccesses()")] - pub struct GetRecordedStorageAccessesCall; + #[ethcall(name = "getStateDiff", abi = "getStateDiff()")] + pub struct GetStateDiffCall; ///Container type for all input parameters for the `isDir` function with signature `isDir(string)` and selector `0x7d15d019` #[derive( Clone, @@ -9792,19 +9739,6 @@ pub mod hevm { )] #[ethcall(name = "record", abi = "record()")] pub struct RecordCall; - ///Container type for all input parameters for the `recordAccountAccesses` function with signature `recordAccountAccesses()` and selector `0xd9391303` - #[derive( - Clone, - ::ethers_contract::EthCall, - ::ethers_contract::EthDisplay, - Default, - Debug, - PartialEq, - Eq, - Hash - )] - #[ethcall(name = "recordAccountAccesses", abi = "recordAccountAccesses()")] - pub struct RecordAccountAccessesCall; ///Container type for all input parameters for the `recordLogs` function with signature `recordLogs()` and selector `0x41af2f52` #[derive( Clone, @@ -9818,7 +9752,7 @@ pub mod hevm { )] #[ethcall(name = "recordLogs", abi = "recordLogs()")] pub struct RecordLogsCall; - ///Container type for all input parameters for the `recordStorageAccesses` function with signature `recordStorageAccesses()` and selector `0x71009b87` + ///Container type for all input parameters for the `recordStateDiff` function with signature `recordStateDiff()` and selector `0xc9140b4b` #[derive( Clone, ::ethers_contract::EthCall, @@ -9829,8 +9763,8 @@ pub mod hevm { Eq, Hash )] - #[ethcall(name = "recordStorageAccesses", abi = "recordStorageAccesses()")] - pub struct RecordStorageAccessesCall; + #[ethcall(name = "recordStateDiff", abi = "recordStateDiff()")] + pub struct RecordStateDiffCall; ///Container type for all input parameters for the `rememberKey` function with signature `rememberKey(uint256)` and selector `0x22100064` #[derive( Clone, @@ -10890,9 +10824,8 @@ pub mod hevm { GetMappingSlotAt(GetMappingSlotAtCall), GetNonce0(GetNonce0Call), GetNonce1(GetNonce1Call), - GetRecordedAccountAccesses(GetRecordedAccountAccessesCall), GetRecordedLogs(GetRecordedLogsCall), - GetRecordedStorageAccesses(GetRecordedStorageAccessesCall), + GetStateDiff(GetStateDiffCall), IsDir(IsDirCall), IsFile(IsFileCall), IsPersistent(IsPersistentCall), @@ -10945,9 +10878,8 @@ pub mod hevm { ReadLine(ReadLineCall), ReadLink(ReadLinkCall), Record(RecordCall), - RecordAccountAccesses(RecordAccountAccessesCall), RecordLogs(RecordLogsCall), - RecordStorageAccesses(RecordStorageAccessesCall), + RecordStateDiff(RecordStateDiffCall), RememberKey(RememberKeyCall), RemoveDir(RemoveDirCall), RemoveFile(RemoveFileCall), @@ -11477,20 +11409,15 @@ pub mod hevm { ) { return Ok(Self::GetNonce1(decoded)); } - if let Ok(decoded) = ::decode( - data, - ) { - return Ok(Self::GetRecordedAccountAccesses(decoded)); - } if let Ok(decoded) = ::decode( data, ) { return Ok(Self::GetRecordedLogs(decoded)); } - if let Ok(decoded) = ::decode( + if let Ok(decoded) = ::decode( data, ) { - return Ok(Self::GetRecordedStorageAccesses(decoded)); + return Ok(Self::GetStateDiff(decoded)); } if let Ok(decoded) = ::decode( data, @@ -11752,20 +11679,15 @@ pub mod hevm { ) { return Ok(Self::Record(decoded)); } - if let Ok(decoded) = ::decode( - data, - ) { - return Ok(Self::RecordAccountAccesses(decoded)); - } if let Ok(decoded) = ::decode( data, ) { return Ok(Self::RecordLogs(decoded)); } - if let Ok(decoded) = ::decode( + if let Ok(decoded) = ::decode( data, ) { - return Ok(Self::RecordStorageAccesses(decoded)); + return Ok(Self::RecordStateDiff(decoded)); } if let Ok(decoded) = ::decode( data, @@ -12318,13 +12240,10 @@ pub mod hevm { Self::GetNonce1(element) => { ::ethers_core::abi::AbiEncode::encode(element) } - Self::GetRecordedAccountAccesses(element) => { - ::ethers_core::abi::AbiEncode::encode(element) - } Self::GetRecordedLogs(element) => { ::ethers_core::abi::AbiEncode::encode(element) } - Self::GetRecordedStorageAccesses(element) => { + Self::GetStateDiff(element) => { ::ethers_core::abi::AbiEncode::encode(element) } Self::IsDir(element) => ::ethers_core::abi::AbiEncode::encode(element), @@ -12453,13 +12372,10 @@ pub mod hevm { Self::ReadLine(element) => ::ethers_core::abi::AbiEncode::encode(element), Self::ReadLink(element) => ::ethers_core::abi::AbiEncode::encode(element), Self::Record(element) => ::ethers_core::abi::AbiEncode::encode(element), - Self::RecordAccountAccesses(element) => { - ::ethers_core::abi::AbiEncode::encode(element) - } Self::RecordLogs(element) => { ::ethers_core::abi::AbiEncode::encode(element) } - Self::RecordStorageAccesses(element) => { + Self::RecordStateDiff(element) => { ::ethers_core::abi::AbiEncode::encode(element) } Self::RememberKey(element) => { @@ -12735,13 +12651,8 @@ pub mod hevm { Self::GetMappingSlotAt(element) => ::core::fmt::Display::fmt(element, f), Self::GetNonce0(element) => ::core::fmt::Display::fmt(element, f), Self::GetNonce1(element) => ::core::fmt::Display::fmt(element, f), - Self::GetRecordedAccountAccesses(element) => { - ::core::fmt::Display::fmt(element, f) - } Self::GetRecordedLogs(element) => ::core::fmt::Display::fmt(element, f), - Self::GetRecordedStorageAccesses(element) => { - ::core::fmt::Display::fmt(element, f) - } + Self::GetStateDiff(element) => ::core::fmt::Display::fmt(element, f), Self::IsDir(element) => ::core::fmt::Display::fmt(element, f), Self::IsFile(element) => ::core::fmt::Display::fmt(element, f), Self::IsPersistent(element) => ::core::fmt::Display::fmt(element, f), @@ -12806,13 +12717,8 @@ pub mod hevm { Self::ReadLine(element) => ::core::fmt::Display::fmt(element, f), Self::ReadLink(element) => ::core::fmt::Display::fmt(element, f), Self::Record(element) => ::core::fmt::Display::fmt(element, f), - Self::RecordAccountAccesses(element) => { - ::core::fmt::Display::fmt(element, f) - } Self::RecordLogs(element) => ::core::fmt::Display::fmt(element, f), - Self::RecordStorageAccesses(element) => { - ::core::fmt::Display::fmt(element, f) - } + Self::RecordStateDiff(element) => ::core::fmt::Display::fmt(element, f), Self::RememberKey(element) => ::core::fmt::Display::fmt(element, f), Self::RemoveDir(element) => ::core::fmt::Display::fmt(element, f), Self::RemoveFile(element) => ::core::fmt::Display::fmt(element, f), @@ -13343,19 +13249,14 @@ pub mod hevm { Self::GetNonce1(value) } } - impl ::core::convert::From for HEVMCalls { - fn from(value: GetRecordedAccountAccessesCall) -> Self { - Self::GetRecordedAccountAccesses(value) - } - } impl ::core::convert::From for HEVMCalls { fn from(value: GetRecordedLogsCall) -> Self { Self::GetRecordedLogs(value) } } - impl ::core::convert::From for HEVMCalls { - fn from(value: GetRecordedStorageAccessesCall) -> Self { - Self::GetRecordedStorageAccesses(value) + impl ::core::convert::From for HEVMCalls { + fn from(value: GetStateDiffCall) -> Self { + Self::GetStateDiff(value) } } impl ::core::convert::From for HEVMCalls { @@ -13618,19 +13519,14 @@ pub mod hevm { Self::Record(value) } } - impl ::core::convert::From for HEVMCalls { - fn from(value: RecordAccountAccessesCall) -> Self { - Self::RecordAccountAccesses(value) - } - } impl ::core::convert::From for HEVMCalls { fn from(value: RecordLogsCall) -> Self { Self::RecordLogs(value) } } - impl ::core::convert::From for HEVMCalls { - fn from(value: RecordStorageAccessesCall) -> Self { - Self::RecordStorageAccesses(value) + impl ::core::convert::From for HEVMCalls { + fn from(value: RecordStateDiffCall) -> Self { + Self::RecordStateDiff(value) } } impl ::core::convert::From for HEVMCalls { @@ -14621,29 +14517,6 @@ pub mod hevm { Hash )] pub struct GetNonce0Return(pub u64); - ///Container type for all return fields from the `getRecordedAccountAccesses` function with signature `getRecordedAccountAccesses()` and selector `0x4452c985` - #[derive( - Clone, - ::ethers_contract::EthAbiType, - ::ethers_contract::EthAbiCodec, - Default, - Debug, - PartialEq, - Eq, - Hash - )] - pub struct GetRecordedAccountAccessesReturn( - pub ::std::vec::Vec< - ( - ::ethers_core::types::Address, - u8, - bool, - ::ethers_core::types::U256, - ::ethers_core::types::Bytes, - bool, - ), - >, - ); ///Container type for all return fields from the `getRecordedLogs` function with signature `getRecordedLogs()` and selector `0x191553a4` #[derive( Clone, @@ -14658,7 +14531,7 @@ pub mod hevm { pub struct GetRecordedLogsReturn( pub ::std::vec::Vec<(::std::vec::Vec<[u8; 32]>, ::ethers_core::types::Bytes)>, ); - ///Container type for all return fields from the `getRecordedStorageAccesses` function with signature `getRecordedStorageAccesses()` and selector `0x03f1500a` + ///Container type for all return fields from the `getStateDiff` function with signature `getStateDiff()` and selector `0x80df01cc` #[derive( Clone, ::ethers_contract::EthAbiType, @@ -14669,9 +14542,30 @@ pub mod hevm { Eq, Hash )] - pub struct GetRecordedStorageAccessesReturn( + pub struct GetStateDiffReturn( pub ::std::vec::Vec< - (::ethers_core::types::Address, [u8; 32], bool, [u8; 32], [u8; 32], bool), + ( + ::ethers_core::types::Address, + ::ethers_core::types::Address, + ::ethers_core::types::U256, + bool, + ::ethers_core::types::U256, + ::ethers_core::types::U256, + ::ethers_core::types::Bytes, + ::ethers_core::types::U256, + ::ethers_core::types::Bytes, + bool, + ::std::vec::Vec< + ( + ::ethers_core::types::Address, + [u8; 32], + bool, + [u8; 32], + [u8; 32], + bool, + ), + >, + ), >, ); ///Container type for all return fields from the `isDir` function with signature `isDir(string)` and selector `0x7d15d019` @@ -15454,7 +15348,7 @@ pub mod hevm { Hash )] pub struct UnixTimeReturn(pub ::ethers_core::types::U256); - ///`AccountAccess(address,uint8,bool,uint256,bytes,bool)` + ///`AccountAccess(address,address,uint256,bool,uint256,uint256,bytes,uint256,bytes,bool,(address,bytes32,bool,bytes32,bytes32,bool)[])` #[derive( Clone, ::ethers_contract::EthAbiType, @@ -15466,12 +15360,17 @@ pub mod hevm { Hash )] pub struct AccountAccess { + pub accessor: ::ethers_core::types::Address, pub account: ::ethers_core::types::Address, - pub kind: u8, + pub kind: ::ethers_core::types::U256, pub initialized: bool, + pub old_balance: ::ethers_core::types::U256, + pub new_balance: ::ethers_core::types::U256, + pub deployed_code: ::ethers_core::types::Bytes, pub value: ::ethers_core::types::U256, pub data: ::ethers_core::types::Bytes, pub reverted: bool, + pub storage_accesses: ::std::vec::Vec, } ///`DirEntry(string,string,uint64,bool,bool)` #[derive( diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json index 668ad8ca595c..a4a785239288 100644 --- a/crates/cheatcodes/assets/cheatcodes.json +++ b/crates/cheatcodes/assets/cheatcodes.json @@ -263,12 +263,17 @@ }, { "name": "AccountAccess", - "description": "The result of a `getRecordedAccountAccess` call.", + "description": "The result of a `getStateDiff` call.", "fields": [ + { + "name": "accessor", + "ty": "address", + "description": "What accessed the account." + }, { "name": "account", "ty": "address", - "description": "The account whose storage was accessed." + "description": "The account whose storage was accessed.\n It's either the account created, callee or a selfdestruct recipient for CREATE, CALL or SELFDESTRUCT." }, { "name": "kind", @@ -280,6 +285,21 @@ "ty": "bool", "description": "If the account is initialized or empty" }, + { + "name": "oldBalance", + "ty": "uint256", + "description": "The previous account balance of the accessed account." + }, + { + "name": "newBalance", + "ty": "uint256", + "description": "The new account balance of the accessed account. This represents the balance change ignoring any reverts that may have occurred." + }, + { + "name": "deployedCode", + "ty": "bytes", + "description": "Code of the account deployed by CREATE." + }, { "name": "value", "ty": "uint256", @@ -294,12 +314,17 @@ "name": "reverted", "ty": "bool", "description": "If this access was in a reverted context" + }, + { + "name": "storageAccesses", + "ty": "StorageAccess[]", + "description": "An ordered list of storage accesses made during an account access operation." } ] }, { "name": "StorageAccess", - "description": "The result of a `getRecordedStorageAccess` call.", + "description": "The storage accessed during an `AccountAccess`.", "fields": [ { "name": "account", @@ -2155,26 +2180,6 @@ "status": "stable", "safety": "safe" }, - { - "func": { - "id": "getRecordedAccountAccesses", - "description": "Returns an ordered array of all account accesses from a `vm.recordAccountAccess` session.", - "declaration": "function getRecordedAccountAccesses() external returns (AccountAccess[] memory accesses);", - "visibility": "external", - "mutability": "", - "signature": "getRecordedAccountAccesses()", - "selector": "0x4452c985", - "selectorBytes": [ - 68, - 82, - 201, - 133 - ] - }, - "group": "evm", - "status": "stable", - "safety": "safe" - }, { "func": { "id": "getRecordedLogs", @@ -2197,18 +2202,18 @@ }, { "func": { - "id": "getRecordedStorageAccesses", - "description": "Returns an ordered array of all storage accesses from a `vm.recordStorageAccess` session.", - "declaration": "function getRecordedStorageAccesses() external returns (StorageAccess[] memory accesses);", + "id": "getStateDiff", + "description": "Returns an ordered array of all account accesses from a `vm.recordStateDiff` session.", + "declaration": "function getStateDiff() external returns (AccountAccess[] memory accesses);", "visibility": "external", "mutability": "", - "signature": "getRecordedStorageAccesses()", - "selector": "0x03f1500a", + "signature": "getStateDiff()", + "selector": "0x80df01cc", "selectorBytes": [ - 3, - 241, - 80, - 10 + 128, + 223, + 1, + 204 ] }, "group": "evm", @@ -3255,26 +3260,6 @@ "status": "stable", "safety": "safe" }, - { - "func": { - "id": "recordAccountAccesses", - "description": "Record all account accesses as part of CREATE or CALL opcodes in order,\nalong with the context of the calls", - "declaration": "function recordAccountAccesses() external;", - "visibility": "external", - "mutability": "", - "signature": "recordAccountAccesses()", - "selector": "0xd9391303", - "selectorBytes": [ - 217, - 57, - 19, - 3 - ] - }, - "group": "evm", - "status": "stable", - "safety": "safe" - }, { "func": { "id": "recordLogs", @@ -3297,18 +3282,18 @@ }, { "func": { - "id": "recordStorageAccesses", - "description": "Record all storage accesses in order, along with the context of the accesses", - "declaration": "function recordStorageAccesses() external;", + "id": "recordStateDiff", + "description": "Record all account accesses as part of CREATE, CALL or SELFDESTRUCT opcodes in order,\nalong with the context of the calls", + "declaration": "function recordStateDiff() external;", "visibility": "external", "mutability": "", - "signature": "recordStorageAccesses()", - "selector": "0x71009b87", + "signature": "recordStateDiff()", + "selector": "0xc9140b4b", "selectorBytes": [ - 113, - 0, - 155, - 135 + 201, + 20, + 11, + 75 ] }, "group": "evm", diff --git a/crates/cheatcodes/defs/src/vm.rs b/crates/cheatcodes/defs/src/vm.rs index cedb23860281..e2f0775bf453 100644 --- a/crates/cheatcodes/defs/src/vm.rs +++ b/crates/cheatcodes/defs/src/vm.rs @@ -144,23 +144,34 @@ interface Vm { bytes stderr; } - /// The result of a `getRecordedAccountAccess` call. + /// The result of a `getStateDiff` call. struct AccountAccess { + /// What accessed the account. + address accessor; /// The account whose storage was accessed. + /// It's either the account created, callee or a selfdestruct recipient for CREATE, CALL or SELFDESTRUCT. address account; /// The kind of account access. AccountAccessKind kind; /// If the account is initialized or empty bool initialized; + /// The previous account balance of the accessed account. + uint256 oldBalance; + /// The new account balance of the accessed account. This represents the balance change ignoring any reverts that may have occurred. + uint256 newBalance; + /// Code of the account deployed by CREATE. + bytes deployedCode; /// Value passed along with the account access uint256 value; /// Input data provided to the CREATE or CALL bytes data; /// If this access was in a reverted context bool reverted; + /// An ordered list of storage accesses made during an account access operation. + StorageAccess[] storageAccesses; } - /// The result of a `getRecordedStorageAccess` call. + /// The storage accessed during an `AccountAccess`. struct StorageAccess { /// The account whose storage was accessed. address account; @@ -208,22 +219,14 @@ interface Vm { #[cheatcode(group = Evm, safety = Safe)] function accesses(address target) external returns (bytes32[] memory readSlots, bytes32[] memory writeSlots); - /// Record all account accesses as part of CREATE or CALL opcodes in order, + /// Record all account accesses as part of CREATE, CALL or SELFDESTRUCT opcodes in order, /// along with the context of the calls #[cheatcode(group = Evm, safety = Safe)] - function recordAccountAccesses() external; + function recordStateDiff() external; - /// Returns an ordered array of all account accesses from a `vm.recordAccountAccess` session. + /// Returns an ordered array of all account accesses from a `vm.recordStateDiff` session. #[cheatcode(group = Evm, safety = Safe)] - function getRecordedAccountAccesses() external returns (AccountAccess[] memory accesses); - - /// Record all storage accesses in order, along with the context of the accesses - #[cheatcode(group = Evm, safety = Safe)] - function recordStorageAccesses() external; - - /// Returns an ordered array of all storage accesses from a `vm.recordStorageAccess` session. - #[cheatcode(group = Evm, safety = Safe)] - function getRecordedStorageAccesses() external returns (StorageAccess[] memory accesses); + function getStateDiff() external returns (AccountAccess[] memory accesses); // -------- Recording Map Writes -------- diff --git a/crates/cheatcodes/src/evm.rs b/crates/cheatcodes/src/evm.rs index 1173163fe69e..0723a34664a0 100644 --- a/crates/cheatcodes/src/evm.rs +++ b/crates/cheatcodes/src/evm.rs @@ -341,33 +341,18 @@ impl Cheatcode for revertToCall { } } -impl Cheatcode for recordAccountAccessesCall { +impl Cheatcode for recordStateDiffCall { fn apply(&self, state: &mut Cheatcodes) -> Result { let Self {} = self; - state.recorded_account_accesses = Some(Default::default()); + state.recorded_account_diffs = Some(Default::default()); Ok(Default::default()) } } -impl Cheatcode for getRecordedAccountAccessesCall { +impl Cheatcode for getStateDiffCall { fn apply(&self, state: &mut Cheatcodes) -> Result { let Self {} = self; - get_recorded_account_accesses(state) - } -} - -impl Cheatcode for recordStorageAccessesCall { - fn apply(&self, state: &mut Cheatcodes) -> Result { - let Self {} = self; - state.recorded_storage_accesses = Some(Default::default()); - Ok(Default::default()) - } -} - -impl Cheatcode for getRecordedStorageAccessesCall { - fn apply(&self, state: &mut Cheatcodes) -> Result { - let Self {} = self; - get_recorded_storage_accesses(state) + get_state_diff(state) } } @@ -439,32 +424,17 @@ pub(super) fn journaled_account<'a, DB: DatabaseExt>( /// array of [AccountAccess]. If there are no accounts were /// recorded as accessed, an abi encoded empty array is returned. /// -/// In the case where `getRecordedAccountAccesses` is called at a lower -/// depth than `recordAccountAccesses`, multiple `Vec` +/// In the case where `getStateDiff` is called at a lower +/// depth than `recordStateDiff`, multiple `Vec` /// will be flattened, preserving the order of the accesses. -fn get_recorded_account_accesses(state: &mut Cheatcodes) -> Result { - let res = state.recorded_account_accesses +fn get_state_diff(state: &mut Cheatcodes) -> Result { + let res = state + .recorded_account_diffs .replace(Default::default()) .unwrap_or_default() .into_iter() .flatten() + .map(|record| record.access) .collect::>(); Ok(res.abi_encode()) } - -/// Consumes recorded storage accesses and returns them as an abi encoded -/// array of [StorageAccess]. If there are no storage accesses recorded, -/// an abi encoded empty array is returned. -/// -/// In the case where `getRecordedStorageAccesses` is called at a lower -/// depth than `recordStorageAccesses`, multiple `Vec` -/// will be flattened, preserving the order of the accesses. -fn get_recorded_storage_accesses(state: &mut Cheatcodes) -> Result { - let res = state.recorded_storage_accesses - .replace(Default::default()) - .unwrap_or_default() - .into_iter() - .flatten() - .collect::>(); - Ok(res.abi_encode()) -} \ No newline at end of file diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index 9402bd20d33e..bfd0dcb48478 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -85,6 +85,14 @@ pub struct BroadcastableTransaction { /// List of transactions that can be broadcasted. pub type BroadcastableTransactions = VecDeque; +#[derive(Debug, Clone)] +pub struct AccountAccess { + /// The account access. + pub access: crate::Vm::AccountAccess, + /// The call depth the account was accessed. + pub depth: u64, +} + /// An EVM inspector that handles calls to various cheatcodes, each with their own behavior. /// /// Cheatcodes can be called by contracts during execution to modify the VM environment, such as @@ -138,10 +146,7 @@ pub struct Cheatcodes { pub accesses: Option, /// Recorded account accesses (calls, creates) by relative call depth - pub recorded_account_accesses: Option>>, - - /// Recorded storage accesses by relative call depth - pub recorded_storage_accesses: Option>>, + pub recorded_account_diffs: Option>>, /// Recorded logs pub recorded_logs: Option>, @@ -232,6 +237,7 @@ impl Cheatcodes { } /// Determines the address of the contract and marks it as allowed + /// Returns the address of the contract created /// /// There may be cheatcodes in the constructor of the new contract, in order to allow them /// automatically we need to determine the new address @@ -401,47 +407,56 @@ impl Inspector for Cheatcodes { } // Record account access via SELFDESTRUCT if `recordAccountAccesses` has been called - if let Some(account_accesses) = &mut self.recorded_account_accesses { + if let Some(account_accesses) = &mut self.recorded_account_diffs { if interpreter.current_opcode() == opcode::SELFDESTRUCT { let target = try_or_continue!(interpreter.stack().peek(0)); // load balance of this account - let balance: U256; + let value: U256; if let Ok((account, _)) = data.journaled_state.load_account(interpreter.contract().address, data.db) { - balance = account.info.balance; + value = account.info.balance; } else { - balance = U256::ZERO; + value = U256::ZERO; } + // previous balance of the target account + let old_balance: U256; // get initialized status of target account let initialized: bool; if let Ok((account, _)) = data.journaled_state.load_account(Address::from(U160::from(target)), data.db) { initialized = account.info.exists(); + old_balance = account.info.balance; } else { initialized = false; + old_balance = U256::ZERO; } // register access for the target account let access = crate::Vm::AccountAccess { + accessor: interpreter.contract().address, account: Address::from(U160::from(target)), kind: crate::Vm::AccountAccessKind::SelfDestruct, initialized, - value: balance, + oldBalance: old_balance, + newBalance: old_balance + value, + value, data: Bytes::new().to_vec(), reverted: false, + deployedCode: Bytes::new().to_vec(), + storageAccesses: Vec::new(), }; // append access if let Some(last) = &mut account_accesses.last_mut() { - last.push(access); + last.push(AccountAccess { access, depth: data.journaled_state.depth() }); } else { - account_accesses.push(vec![access]); + unreachable!("selfdestruct in a non-existent call frame"); } } } - // Record granular ordered storage accesses if `recordStorageAccesses` has been called - if let Some(recorded_storage_accesses) = &mut self.recorded_storage_accesses { + // Record granular ordered storage accesses if `recordStateDiff` has been called + if let Some(recorded_account_diffs) = &mut self.recorded_account_diffs { match interpreter.current_opcode() { opcode::SLOAD => { let key = try_or_continue!(interpreter.stack().peek(0)); @@ -466,15 +481,18 @@ impl Inspector for Cheatcodes { newValue: present_value.into(), reverted: false, }; - // If there is a "last" entry in the recorded storage accesses, push the current // access onto it - if let Some(last) = &mut recorded_storage_accesses.last_mut() { - last.push(access); - } else { - // Otherwise, append this access as a one-element vector to the empty 2d - // vector - recorded_storage_accesses.push(vec![access]); + if let Some(last) = recorded_account_diffs.last_mut() { + if let Some(last) = last.last_mut() { + // Assert that we're on the correct depth before recording post-call + // state changes. Depending on the depth the + // cheat was called at, there may not be any pending calls + // to update if execution has percolated up to a higher depth. + if last.depth < data.journaled_state.depth() { + last.access.storageAccesses.push(access); + } + } } } opcode::SSTORE => { @@ -501,12 +519,16 @@ impl Inspector for Cheatcodes { }; // If there is a "last" entry in the recorded storage accesses, push the current // access onto it - if let Some(last) = &mut recorded_storage_accesses.last_mut() { - last.push(access); - } else { - // Otherwise, append this access as a one-element vector to the empty 2d - // vector - recorded_storage_accesses.push(vec![access]); + if let Some(last) = recorded_account_diffs.last_mut() { + if let Some(last) = last.last_mut() { + // Assert that we're on the correct depth before recording post-call + // state changes. Depending on the depth the + // cheat was called at, there may not be any pending calls + // to update if execution has percolated up to a higher depth. + if last.depth < data.journaled_state.depth() { + last.access.storageAccesses.push(access); + } + } } } _ => (), @@ -810,40 +832,42 @@ impl Inspector for Cheatcodes { } } - // Record called accounts if `recordAccountAccesses` has been called - if let Some(recorded_account_accesses) = &mut self.recorded_account_accesses { + // Record called accounts if `recordStateDiff` has been called + if let Some(recorded_account_diffs) = &mut self.recorded_account_diffs { // Determine if account is "initialized," ie, it has a non-zero balance, a non-zero // nonce, a non-zero KECCAK_EMPTY codehash, or non-empty code let initialized; + let old_balance; if let Ok((acc, _)) = data.journaled_state.load_account(call.contract, data.db) { initialized = acc.info.exists(); + old_balance = acc.info.balance; } else { initialized = false; + old_balance = U256::ZERO; } // Record this call by pushing it to a new pending vector; all subsequent calls at // that depth will be pushed to the same vector. When the call ends, the // RecordedAccountAccess (and all subsequent RecordedAccountAccesses) will be // updated with the revert status of this call, since the EVM does not mark accounts // as "warm" if the call from which they were accessed is reverted - recorded_account_accesses.push(vec![crate::Vm::AccountAccess { - account: call.contract, - kind: crate::Vm::AccountAccessKind::Call, - initialized, - value: call.transfer.value, - data: call.input.to_vec(), - reverted: false, + recorded_account_diffs.push(vec![AccountAccess { + access: crate::Vm::AccountAccess { + accessor: call.context.caller, + account: call.contract, + kind: crate::Vm::AccountAccessKind::Call, + initialized, + oldBalance: old_balance, + newBalance: U256::ZERO, // updated on call_end + value: call.transfer.value, + data: call.input.to_vec(), + reverted: false, + deployedCode: Bytes::new().to_vec(), + storageAccesses: Vec::new(), // updated on step + }, + depth: data.journaled_state.depth(), }]); } - if let Some(recorded_storage_accesses) = &mut self.recorded_storage_accesses { - // Record this call by pushing it to a new pending vector; all subsequent calls at - // that depth will be pushed to the same vector. When the call ends, the - // RecordedStorageAccess (and all subsequent RecordedStorageAccesses) will be - // updated with the revert status of this call, since the EVM does not mark storage - // as "warm" if the call from which it was accessed is reverted - recorded_storage_accesses.push(Vec::new()); - } - (InstructionResult::Continue, gas, Bytes::new()) } @@ -910,46 +934,46 @@ impl Inspector for Cheatcodes { } } - // If `recordAccountAccesses` has been called, update the `reverted` status of the previous + // If `recordStateDiff` has been called, update the `reverted` status of the previous // call depth's recorded accesses, if any - if let Some(recorded_account_accesses) = &mut self.recorded_account_accesses { - // Depending on the depth the cheat was called at, there may not be any pending calls - // to update if execution has percolated up to a higher depth - if let Some(last_recorded_depth) = &mut recorded_account_accesses.pop() { + if let Some(recorded_account_diffs) = &mut self.recorded_account_diffs { + // The root call cannot be recorded. + if data.journaled_state.depth() > 0 { + let mut last_recorded_depth = + recorded_account_diffs.pop().expect("missing CALL account accesses"); // Update the reverted status of all deeper calls if this call reverted, in // accordance with EVM behavior if status.is_revert() { - last_recorded_depth.iter_mut().for_each(|element| element.reverted = true) - } - // Merge the last depth's AccountAccesses into the AccountAccesses at the current - // depth, or push them back onto the pending vector if higher depths were not - // recorded. This preserves ordering of accesses. - if let Some(last) = recorded_account_accesses.last_mut() { - last.append(last_recorded_depth); - } else { - recorded_account_accesses.push(last_recorded_depth.to_vec()); + last_recorded_depth.iter_mut().for_each(|element| { + element.access.reverted = true; + element + .access + .storageAccesses + .iter_mut() + .for_each(|storage_access| storage_access.reverted = true); + }) } - } - } - - // If `recordStorageAccesses` has been called, update the `reverted` status of the previous - // call depth's recorded accesses, if any - if let Some(recorded_storage_accesses) = &mut self.recorded_storage_accesses { - // Depending on what depth the cheat was called at, there may not be any pending storage - // accesses to update if execution has percolated up to a higher depth - if let Some(last_depth) = &mut recorded_storage_accesses.pop() { - // Update the reverted status of all deeper accesses if this call reverted, in - // accordance with EVM behavior - if status.is_revert() { - last_depth.iter_mut().for_each(|element| element.reverted = true) + let call_access = last_recorded_depth.first_mut().expect("empty AccountAccesses"); + // Assert that we're on the correct depth before recording post-call state changes. + // Depending on the depth the cheat was called at, there may not be any pending + // calls to update if execution has percolated up to a higher depth. + if call_access.depth == data.journaled_state.depth() { + if let Ok((acc, _)) = data.journaled_state.load_account(call.contract, data.db) + { + debug_assert!( + call_access.access.kind as u8 == + crate::Vm::AccountAccessKind::Call as u8 + ); + call_access.access.newBalance = acc.info.balance; + } } - // Merge the last depth's StorageAccesses into the StorageAccesses at the current + // Merge the last depth's AccountAccesses into the AccountAccesses at the current // depth, or push them back onto the pending vector if higher depths were not // recorded. This preserves ordering of accesses. - if let Some(previous_depth) = recorded_storage_accesses.last_mut() { - previous_depth.append(last_depth); + if let Some(last) = recorded_account_diffs.last_mut() { + last.append(&mut last_recorded_depth); } else { - recorded_storage_accesses.push(last_depth.to_vec()); + recorded_account_diffs.push(last_recorded_depth); } } } @@ -1165,27 +1189,27 @@ impl Inspector for Cheatcodes { } // If `recordAccountAccesses` has been called, record the create - if let Some(recorded_account_accesses) = &mut self.recorded_account_accesses { + if let Some(recorded_account_diffs) = &mut self.recorded_account_diffs { // Record the create context as an account access and create a new vector to record all // subsequent account accesses - recorded_account_accesses.push(vec![crate::Vm::AccountAccess { - account: address, - kind: crate::Vm::AccountAccessKind::Create, - initialized: true, - value: call.value, - data: call.init_code.to_vec(), - reverted: false, + recorded_account_diffs.push(vec![AccountAccess { + access: crate::Vm::AccountAccess { + accessor: call.caller, + account: address, + kind: crate::Vm::AccountAccessKind::Create, + initialized: true, + oldBalance: U256::ZERO, // updated on create_end + newBalance: U256::ZERO, // updated on create_end + value: call.value, + data: call.init_code.to_vec(), + reverted: false, + deployedCode: Bytes::new().to_vec(), // updated on create_end + storageAccesses: Vec::new(), // updated on create_end + }, + depth: data.journaled_state.depth(), }]); } - // If `recordStorageAccesses` has been called, push a new vector onto to record - // all accesses at the new execution depth - if let Some(recorded_storage_accesses) = &mut self.recorded_storage_accesses { - // Create a new vector to record all subsequent storage accesses from within the create - // context - recorded_storage_accesses.push(Vec::new()); - } - (InstructionResult::Continue, None, gas, Bytes::new()) } @@ -1242,45 +1266,57 @@ impl Inspector for Cheatcodes { } } - // If `recordAccountAccesses` has been called, update the `reverted` status of the previous + // If `recordStateDiff` has been called, update the `reverted` status of the previous // call depth's recorded accesses, if any - if let Some(recorded_account_accesses) = &mut self.recorded_account_accesses { - // Depending on what depth the cheat was called at, there may not be any pending calls - // to update if execution has percolated up to a higher depth - if let Some(last_depth) = &mut recorded_account_accesses.pop() { + if let Some(recorded_account_diffs) = &mut self.recorded_account_diffs { + // The root call cannot be recorded. + if data.journaled_state.depth() > 0 { + let mut last_depth = + recorded_account_diffs.pop().expect("missing CREATE account accesses"); // Update the reverted status of all deeper calls if this call reverted, in // accordance with EVM behavior if status.is_revert() { - last_depth.iter_mut().for_each(|element| element.reverted = true) - } - // Merge the last depth's AccountAccesses into the AccountAccesses at the current - // depth, or push them back onto the pending vector if higher depths were not - // recorded. This preserves ordering of accesses. - if let Some(last) = recorded_account_accesses.last_mut() { - last.append(last_depth); - } else { - recorded_account_accesses.push(last_depth.to_vec()); + last_depth.iter_mut().for_each(|element| { + element.access.reverted = true; + element + .access + .storageAccesses + .iter_mut() + .for_each(|storage_access| storage_access.reverted = true); + }) } - } - } - - // If `recordStorageAccesses` has been called, update the `reverted` status of the previous - if let Some(recorded_storage_accesses) = &mut self.recorded_storage_accesses { - // Depending on what depth the cheat was called at, there may not be any pending storage - // accesses to update if execution has percolated up to a higher depth - if let Some(last_depth) = &mut recorded_storage_accesses.pop() { - // Update the reverted status of all deeper accesses if this call reverted, in - // accordance with EVM behavior - if status.is_revert() { - last_depth.iter_mut().for_each(|element| element.reverted = true) + let create_access = last_depth.first_mut().expect("empty AccountAccesses"); + // Assert that we're on the correct depth before recording post-create state + // changes. Depending on what depth the cheat was called at, there + // may not be any pending calls to update if execution has + // percolated up to a higher depth. + if create_access.depth == data.journaled_state.depth() { + debug_assert!( + create_access.access.kind as u8 == + crate::Vm::AccountAccessKind::Create as u8 + ); + if let Some(address) = address { + if let Ok((created_acc, _)) = + data.journaled_state.load_account(address, data.db) + { + create_access.access.newBalance = created_acc.info.balance; + create_access.access.deployedCode = created_acc + .info + .code + .clone() + .unwrap_or_default() + .original_bytes() + .into(); + } + } } - // Merge the last depth's StorageAccesses into the StorageAccesses at the current + // Merge the last depth's AccountAccesses into the AccountAccesses at the current // depth, or push them back onto the pending vector if higher depths were not // recorded. This preserves ordering of accesses. - if let Some(last) = recorded_storage_accesses.last_mut() { - last.append(last_depth); + if let Some(last) = recorded_account_diffs.last_mut() { + last.append(&mut last_depth); } else { - recorded_storage_accesses.push(last_depth.to_vec()); + recorded_account_diffs.push(last_depth); } } } diff --git a/testdata/cheats/RecordAccountAccesses.t.sol b/testdata/cheats/RecordAccountAccesses.t.sol index 99397cb13fb7..69146eb82d6e 100644 --- a/testdata/cheats/RecordAccountAccesses.t.sol +++ b/testdata/cheats/RecordAccountAccesses.t.sol @@ -9,7 +9,7 @@ import "./Vm.sol"; contract SelfCaller { constructor(bytes memory) payable { assembly { - // call self to test that the cheatcote correctly reports the + // call self to test that the cheatcode correctly reports the // account as initialized even when there is no code at the // contract address pop(call(gas(), address(), div(callvalue(), 10), 0, 0, 0, 0)) @@ -18,13 +18,30 @@ contract SelfCaller { } } +/// @notice Helper contract with a constructor that stores a value in storage +/// and then optionally reverts. +contract ConstructorStorer { + constructor(bool shouldRevert) { + assembly { + sstore(0x00, 0x01) + if shouldRevert { revert(0, 0) } + } + } +} + /// @notice Helper contract that calls itself from the run method contract Doer { + uint256[10] spacer; + mapping(bytes32 key => uint256 value) slots; + function run() public payable { + slots[bytes32("doer 1")]++; this.doStuff{value: msg.value / 10}(); } - function doStuff() external payable {} + function doStuff() external payable { + slots[bytes32("doer 2")]++; + } } /// @notice Helper contract that selfdestructs to a target address within its @@ -48,12 +65,14 @@ contract Create2or { /// reverts contract Reverter { Doer immutable doer; + mapping(bytes32 key => uint256 value) slots; constructor(Doer _doer) { doer = _doer; } function run() public payable { + slots[bytes32("reverter")]++; doer.run{value: msg.value / 10}(); revert(); } @@ -62,12 +81,14 @@ contract Reverter { /// @notice Helper contract that calls a Doer from the run method contract Succeeder { Doer immutable doer; + mapping(bytes32 key => uint256 value) slots; constructor(Doer _doer) { doer = _doer; } function run() public payable { + slots[bytes32("succeeder")]++; doer.run{value: msg.value / 10}(); } } @@ -78,6 +99,7 @@ contract NestedRunner { Doer public immutable doer; Reverter public immutable reverter; Succeeder public immutable succeeder; + mapping(bytes32 key => uint256 value) slots; constructor() { doer = new Doer(); @@ -86,6 +108,7 @@ contract NestedRunner { } function run(bool shouldRevert) public payable { + slots[bytes32("runner")]++; try reverter.run{value: msg.value / 10}() { if (shouldRevert) { revert(); @@ -98,20 +121,111 @@ contract NestedRunner { } } +/// @notice Helper contract that directly reads from and writes to storage +contract StorageAccessor { + function read(bytes32 slot) public view returns (bytes32 value) { + assembly { + value := sload(slot) + } + } + + function write(bytes32 slot, bytes32 value) public { + assembly { + sstore(slot, value) + } + } +} + /// @notice Test that the cheatcode correctly records account accesses contract RecordAccountAccessesTest is DSTest { Vm constant cheats = Vm(HEVM_ADDRESS); NestedRunner runner; Create2or create2or; + StorageAccessor test1; + StorageAccessor test2; function setUp() public { runner = new NestedRunner(); create2or = new Create2or(); + test1 = new StorageAccessor(); + test2 = new StorageAccessor(); + } + + /// @notice Test normal, non-nested storage accesses + function testStorageAccesses() public { + StorageAccessor one = test1; + StorageAccessor two = test2; + cheats.recordStateDiff(); + + one.read(bytes32(uint256(1234))); + one.write(bytes32(uint256(1235)), bytes32(uint256(5678))); + two.write(bytes32(uint256(5678)), bytes32(uint256(123469))); + two.write(bytes32(uint256(5678)), bytes32(uint256(1234))); + + Vm.AccountAccess[] memory called = cheats.getStateDiff(); + // Empty since no record account is associated with the storage access + assertEq(called.length, 4, "incorrect length"); + + assertEq(called[0].storageAccesses.length, 1, "incorrect length"); + Vm.StorageAccess memory access = called[0].storageAccesses[0]; + assertEq( + access, + Vm.StorageAccess({ + account: address(one), + slot: bytes32(uint256(1234)), + isWrite: false, + previousValue: bytes32(uint256(0)), + newValue: bytes32(uint256(0)), + reverted: false + }) + ); + + assertEq(called[1].storageAccesses.length, 1, "incorrect length"); + access = called[1].storageAccesses[0]; + assertEq( + access, + Vm.StorageAccess({ + account: address(one), + slot: bytes32(uint256(1235)), + isWrite: true, + previousValue: bytes32(uint256(0)), + newValue: bytes32(uint256(5678)), + reverted: false + }) + ); + + assertEq(called[2].storageAccesses.length, 1, "incorrect length"); + access = called[2].storageAccesses[0]; + assertEq( + access, + Vm.StorageAccess({ + account: address(two), + slot: bytes32(uint256(5678)), + isWrite: true, + previousValue: bytes32(uint256(0)), + newValue: bytes32(uint256(123469)), + reverted: false + }) + ); + + assertEq(called[3].storageAccesses.length, 1, "incorrect length"); + access = called[3].storageAccesses[0]; + assertEq( + access, + Vm.StorageAccess({ + account: address(two), + slot: bytes32(uint256(5678)), + isWrite: true, + previousValue: bytes32(uint256(123469)), + newValue: bytes32(uint256(1234)), + reverted: false + }) + ); } /// @notice Test that basic account accesses are correctly recorded function testRecordAccountAccesses() public { - cheats.recordAccountAccesses(); + cheats.recordStateDiff(); (bool succ,) = address(1234).call(""); (succ,) = address(5678).call{value: 1 ether}(""); @@ -120,73 +234,103 @@ contract RecordAccountAccessesTest is DSTest { // contract calls to self in constructor SelfCaller caller = new SelfCaller{value: 2 ether}('hello2 world2'); - Vm.AccountAccess[] memory called = cheats.getRecordedAccountAccesses(); + Vm.AccountAccess[] memory called = cheats.getStateDiff(); assertEq(called.length, 6); assertEq( called[0], Vm.AccountAccess({ + accessor: address(this), account: address(1234), kind: Vm.AccountAccessKind.Call, initialized: false, + oldBalance: 0, + newBalance: 0, + deployedCode: hex"", value: 0, data: "", - reverted: false + reverted: false, + storageAccesses: new Vm.StorageAccess[](0) }) ); assertEq( called[1], Vm.AccountAccess({ + accessor: address(this), account: address(5678), kind: Vm.AccountAccessKind.Call, initialized: false, + oldBalance: 0, + newBalance: 1 ether, + deployedCode: hex"", value: 1 ether, data: "", - reverted: false + reverted: false, + storageAccesses: new Vm.StorageAccess[](0) }) ); assertEq( called[2], Vm.AccountAccess({ + accessor: address(this), account: address(123469), kind: Vm.AccountAccessKind.Call, initialized: false, + oldBalance: 0, + newBalance: 0, + deployedCode: hex"", value: 0, data: "hello world", - reverted: false + reverted: false, + storageAccesses: new Vm.StorageAccess[](0) }) ); assertEq( called[3], Vm.AccountAccess({ + accessor: address(this), account: address(5678), kind: Vm.AccountAccessKind.Call, initialized: true, + oldBalance: 1 ether, + newBalance: 1 ether, + deployedCode: hex"", value: 0, data: "", - reverted: false + reverted: false, + storageAccesses: new Vm.StorageAccess[](0) }) ); assertEq( called[4], Vm.AccountAccess({ + accessor: address(this), account: address(caller), kind: Vm.AccountAccessKind.Create, initialized: true, + oldBalance: 0, + newBalance: 2 ether, + deployedCode: address(caller).code, value: 2 ether, data: abi.encodePacked(type(SelfCaller).creationCode, abi.encode("hello2 world2")), - reverted: false + reverted: false, + storageAccesses: new Vm.StorageAccess[](0) }) ); assertEq( called[5], Vm.AccountAccess({ + accessor: address(caller), account: address(caller), kind: Vm.AccountAccessKind.Call, initialized: true, + oldBalance: 2 ether, + newBalance: 2 ether, + deployedCode: hex"", value: 0.2 ether, data: "", - reverted: false + reverted: false, + storageAccesses: new Vm.StorageAccess[](0) }) ); } @@ -194,44 +338,55 @@ contract RecordAccountAccessesTest is DSTest { /// @notice Test that account accesses are correctly recorded when a call /// reverts function testRevertingCall() public { - cheats.recordAccountAccesses(); + uint256 initBalance = address(this).balance; + cheats.recordStateDiff(); try this.revertingCall{value: 1 ether}(address(1234), "") {} catch {} - Vm.AccountAccess[] memory called = cheats.getRecordedAccountAccesses(); + Vm.AccountAccess[] memory called = cheats.getStateDiff(); assertEq(called.length, 2); assertEq( called[0], Vm.AccountAccess({ + accessor: address(this), account: address(this), kind: Vm.AccountAccessKind.Call, initialized: true, + oldBalance: initBalance, + newBalance: initBalance, + deployedCode: hex"", value: 1 ether, data: abi.encodeCall(this.revertingCall, (address(1234), "")), - reverted: true + reverted: true, + storageAccesses: new Vm.StorageAccess[](0) }) ); assertEq( called[1], Vm.AccountAccess({ + accessor: address(this), account: address(1234), kind: Vm.AccountAccessKind.Call, initialized: false, + oldBalance: 0, + newBalance: 0.1 ether, + deployedCode: hex"", value: 0.1 ether, data: "", - reverted: true + reverted: true, + storageAccesses: new Vm.StorageAccess[](0) }) ); } /// @notice Test that nested account accesses are correctly recorded function testNested() public { - cheats.recordAccountAccesses(); + cheats.recordStateDiff(); runNested(false, false); } /// @notice Test that nested account accesses are correctly recorded when /// the first call reverts function testNested_Revert() public { - cheats.recordAccountAccesses(); + cheats.recordStateDiff(); runNested(true, false); } @@ -239,99 +394,302 @@ contract RecordAccountAccessesTest is DSTest { /// @param shouldRevert Whether the first call should revert function runNested(bool shouldRevert, bool expectFirstCall) public { try runner.run{value: 1 ether}(shouldRevert) {} catch {} - Vm.AccountAccess[] memory called = cheats.getRecordedAccountAccesses(); + Vm.AccountAccess[] memory called = cheats.getStateDiff(); assertEq(called.length, 7 + toUint(expectFirstCall), "incorrect length"); + + uint256 startingIndex = toUint(expectFirstCall); if (expectFirstCall) { assertEq( called[0], Vm.AccountAccess({ + accessor: address(this), account: address(1234), kind: Vm.AccountAccessKind.Call, + oldBalance: 0, + newBalance: 0, + deployedCode: "", initialized: false, value: 0, data: "", - reverted: false + reverted: false, + storageAccesses: new Vm.StorageAccess[](0) }) ); } - uint256 startingIndex = toUint(expectFirstCall); + assertEq(called[startingIndex].storageAccesses.length, 2, "incorrect length"); + assertIncrementEq( + called[startingIndex].storageAccesses[0], + called[startingIndex].storageAccesses[1], + Vm.StorageAccess({ + account: address(runner), + slot: keccak256(abi.encodePacked(bytes32("runner"), bytes32(0))), + isWrite: true, + previousValue: bytes32(uint256(0)), + newValue: bytes32(uint256(1)), + reverted: shouldRevert + }) + ); assertEq( called[startingIndex], Vm.AccountAccess({ + accessor: address(this), account: address(runner), kind: Vm.AccountAccessKind.Call, + oldBalance: 0, + newBalance: shouldRevert ? 0 : 0.9 ether, + deployedCode: "", initialized: true, value: 1 ether, data: abi.encodeCall(NestedRunner.run, (shouldRevert)), - reverted: shouldRevert + reverted: shouldRevert, + storageAccesses: new Vm.StorageAccess[](0) + }), + false + ); + + assertEq(called[startingIndex + 1].storageAccesses.length, 2, "incorrect length"); + assertIncrementEq( + called[startingIndex + 1].storageAccesses[0], + called[startingIndex + 1].storageAccesses[1], + Vm.StorageAccess({ + account: address(runner.reverter()), + slot: keccak256(abi.encodePacked(bytes32("reverter"), bytes32(0))), + isWrite: true, + previousValue: bytes32(uint256(0)), + newValue: bytes32(uint256(1)), + reverted: true }) ); assertEq( called[startingIndex + 1], Vm.AccountAccess({ + accessor: address(runner), account: address(runner.reverter()), kind: Vm.AccountAccessKind.Call, + oldBalance: 0, + newBalance: 0, + deployedCode: "", initialized: true, value: 0.1 ether, data: abi.encodeCall(Reverter.run, ()), - reverted: true + reverted: true, + storageAccesses: new Vm.StorageAccess[](0) + }), + false + ); + + assertEq(called[startingIndex + 2].storageAccesses.length, 2, "incorrect length"); + assertIncrementEq( + called[startingIndex + 2].storageAccesses[0], + called[startingIndex + 2].storageAccesses[1], + Vm.StorageAccess({ + account: address(runner.doer()), + slot: keccak256(abi.encodePacked(bytes32("doer 1"), uint256(10))), + isWrite: true, + previousValue: bytes32(uint256(0)), + newValue: bytes32(uint256(1)), + reverted: true }) ); assertEq( called[startingIndex + 2], Vm.AccountAccess({ + accessor: address(runner.reverter()), account: address(runner.doer()), kind: Vm.AccountAccessKind.Call, + oldBalance: 0, + newBalance: 0.01 ether, + deployedCode: "", initialized: true, value: 0.01 ether, data: abi.encodeCall(Doer.run, ()), - reverted: true + reverted: true, + storageAccesses: new Vm.StorageAccess[](0) + }), + false + ); + + assertEq(called[startingIndex + 3].storageAccesses.length, 2, "incorrect length"); + assertIncrementEq( + called[startingIndex + 3].storageAccesses[0], + called[startingIndex + 3].storageAccesses[1], + Vm.StorageAccess({ + account: address(runner.doer()), + slot: keccak256(abi.encodePacked(bytes32("doer 2"), uint256(10))), + isWrite: true, + previousValue: bytes32(uint256(0)), + newValue: bytes32(uint256(1)), + reverted: true }) ); assertEq( called[startingIndex + 3], Vm.AccountAccess({ + accessor: address(runner.doer()), account: address(runner.doer()), kind: Vm.AccountAccessKind.Call, + oldBalance: 0.01 ether, + newBalance: 0.01 ether, + deployedCode: "", initialized: true, value: 0.001 ether, data: abi.encodeCall(Doer.doStuff, ()), - reverted: true - }) + reverted: true, + storageAccesses: new Vm.StorageAccess[](0) + }), + false ); + assertEq(called[startingIndex + 4].storageAccesses.length, 2, "incorrect length"); + assertIncrementEq( + called[startingIndex + 4].storageAccesses[0], + called[startingIndex + 4].storageAccesses[1], + Vm.StorageAccess({ + account: address(runner.succeeder()), + slot: keccak256(abi.encodePacked(bytes32("succeeder"), uint256(0))), + isWrite: true, + previousValue: bytes32(uint256(0)), + newValue: bytes32(uint256(1)), + reverted: shouldRevert + }) + ); assertEq( called[startingIndex + 4], Vm.AccountAccess({ + accessor: address(runner), account: address(runner.succeeder()), kind: Vm.AccountAccessKind.Call, + oldBalance: 0, + newBalance: 0.09 ether, + deployedCode: "", initialized: true, value: 0.1 ether, data: abi.encodeCall(Succeeder.run, ()), - reverted: shouldRevert + reverted: shouldRevert, + storageAccesses: new Vm.StorageAccess[](0) + }), + false + ); + + assertEq(called[startingIndex + 5].storageAccesses.length, 2, "incorrect length"); + assertIncrementEq( + called[startingIndex + 5].storageAccesses[0], + called[startingIndex + 5].storageAccesses[1], + Vm.StorageAccess({ + account: address(runner.doer()), + slot: keccak256(abi.encodePacked(bytes32("doer 1"), uint256(10))), + isWrite: true, + previousValue: bytes32(uint256(0)), + newValue: bytes32(uint256(1)), + reverted: shouldRevert }) ); assertEq( called[startingIndex + 5], Vm.AccountAccess({ + accessor: address(runner.succeeder()), account: address(runner.doer()), kind: Vm.AccountAccessKind.Call, + oldBalance: 0, + newBalance: 0.01 ether, + deployedCode: "", initialized: true, value: 0.01 ether, data: abi.encodeCall(Doer.run, ()), - reverted: shouldRevert + reverted: shouldRevert, + storageAccesses: new Vm.StorageAccess[](0) + }), + false + ); + + assertEq(called[startingIndex + 3].storageAccesses.length, 2, "incorrect length"); + assertIncrementEq( + called[startingIndex + 6].storageAccesses[0], + called[startingIndex + 6].storageAccesses[1], + Vm.StorageAccess({ + account: address(runner.doer()), + slot: keccak256(abi.encodePacked(bytes32("doer 2"), uint256(10))), + isWrite: true, + previousValue: bytes32(uint256(0)), + newValue: bytes32(uint256(1)), + reverted: shouldRevert }) ); assertEq( called[startingIndex + 6], Vm.AccountAccess({ + accessor: address(runner.doer()), account: address(runner.doer()), kind: Vm.AccountAccessKind.Call, + oldBalance: 0.01 ether, + newBalance: 0.01 ether, + deployedCode: "", initialized: true, value: 0.001 ether, data: abi.encodeCall(Doer.doStuff, ()), - reverted: shouldRevert + reverted: shouldRevert, + storageAccesses: new Vm.StorageAccess[](0) + }), + false + ); + } + + /// @notice Test that constructor account and storage accesses are recorded, including reverts + function testConstructorStorage() public { + cheats.recordStateDiff(); + address storer = address(new ConstructorStorer(false)); + try create2or.create2(bytes32(0), abi.encodePacked(type(ConstructorStorer).creationCode, abi.encode(true))) {} + catch {} + bytes memory creationCode = abi.encodePacked(type(ConstructorStorer).creationCode, abi.encode(true)); + address hypotheticalStorer = deriveCreate2Address(address(create2or), bytes32(0), keccak256(creationCode)); + + Vm.AccountAccess[] memory called = cheats.getStateDiff(); + assertEq(called.length, 3, "incorrect account access length"); + assertEq(toUint(called[0].kind), toUint(Vm.AccountAccessKind.Create), "incorrect kind"); + assertEq(toUint(called[1].kind), toUint(Vm.AccountAccessKind.Call), "incorrect kind"); + assertEq(toUint(called[2].kind), toUint(Vm.AccountAccessKind.Create), "incorrect kind"); + + Vm.StorageAccess[] memory storageAccesses = new Vm.StorageAccess[](1); + + assertEq(called[0].storageAccesses.length, 1, "incorrect storage access length"); + storageAccesses[0] = Vm.StorageAccess({ + account: storer, + slot: bytes32(uint256(0)), + isWrite: true, + previousValue: bytes32(uint256(0)), + newValue: bytes32(uint256(1)), + reverted: false + }); + assertEq( + called[0], + Vm.AccountAccess({ + accessor: address(this), + account: address(storer), + kind: Vm.AccountAccessKind.Create, + oldBalance: 0, + newBalance: 0, + deployedCode: storer.code, + initialized: true, + value: 0, + data: abi.encodePacked(type(ConstructorStorer).creationCode, abi.encode(false)), + reverted: false, + storageAccesses: storageAccesses + }) + ); + + assertEq(called[1].storageAccesses.length, 0, "incorrect storage access length"); + + assertEq(called[2].storageAccesses.length, 1, "incorrect storage access length"); + assertEq( + called[2].storageAccesses[0], + Vm.StorageAccess({ + account: hypotheticalStorer, + slot: bytes32(uint256(0)), + isWrite: true, + previousValue: bytes32(uint256(0)), + newValue: bytes32(uint256(1)), + reverted: true }) ); } @@ -340,41 +698,57 @@ contract RecordAccountAccessesTest is DSTest { /// recording is started from a lower depth than they are /// retrieved function testNested_LowerDepth() public { - this.startRecordingFromLowerDepth(); - runNested(true, true); this.startRecordingFromLowerDepth(); runNested(false, true); } + /// @notice Test that account accesses are correctly recorded when + /// the first call reverts the and recording is started from + /// a lower depth than they are retrieved. + function testNested_LowerDepth_Revert() public { + this.startRecordingFromLowerDepth(); + runNested(true, true); + } + /// @notice Test that constructor calls and calls made within a constructor /// are correctly recorded, even if it reverts function testCreateRevert() public { - cheats.recordAccountAccesses(); + cheats.recordStateDiff(); bytes memory creationCode = abi.encodePacked(type(SelfCaller).creationCode, abi.encode("")); try create2or.create2(bytes32(0), creationCode) {} catch {} address hypotheticalAddress = deriveCreate2Address(address(create2or), bytes32(0), keccak256(creationCode)); - Vm.AccountAccess[] memory called = cheats.getRecordedAccountAccesses(); + Vm.AccountAccess[] memory called = cheats.getStateDiff(); assertEq(called.length, 3, "incorrect length"); assertEq( called[1], Vm.AccountAccess({ + accessor: address(create2or), account: hypotheticalAddress, kind: Vm.AccountAccessKind.Create, + oldBalance: 0, + newBalance: 0, + deployedCode: address(hypotheticalAddress).code, initialized: true, value: 0, data: creationCode, - reverted: true + reverted: false, + storageAccesses: new Vm.StorageAccess[](0) }) ); assertEq( called[2], Vm.AccountAccess({ + accessor: hypotheticalAddress, account: hypotheticalAddress, kind: Vm.AccountAccessKind.Call, + oldBalance: 0, + newBalance: 0, + deployedCode: hex"", initialized: true, value: 0, data: "", - reverted: true + reverted: false, + storageAccesses: new Vm.StorageAccess[](0) }) ); } @@ -383,59 +757,80 @@ contract RecordAccountAccessesTest is DSTest { /// are public networks that support the opcode, regardless of whether /// or not Ethereum mainnet does. function testSelfDestruct() public { + uint256 startingBalance = address(this).balance; this.startRecordingFromLowerDepth(); address a = address(new SelfDestructor{value:1 ether}(address(this))); address b = address(new SelfDestructor{value:1 ether}(address(bytes20("doesn't exist yet")))); - Vm.AccountAccess[] memory called = cheats.getRecordedAccountAccesses(); + Vm.AccountAccess[] memory called = cheats.getStateDiff(); assertEq(called.length, 5, "incorrect length"); assertEq( called[1], Vm.AccountAccess({ + accessor: address(this), account: a, kind: Vm.AccountAccessKind.Create, + oldBalance: 0, + newBalance: 0, + deployedCode: "", initialized: true, value: 1 ether, data: abi.encodePacked(type(SelfDestructor).creationCode, abi.encode(address(this))), - reverted: false + reverted: false, + storageAccesses: new Vm.StorageAccess[](0) }) ); assertEq( called[2], Vm.AccountAccess({ + accessor: address(a), account: address(this), kind: Vm.AccountAccessKind.SelfDestruct, + oldBalance: startingBalance - 1 ether, + newBalance: startingBalance, + deployedCode: "", initialized: true, value: 1 ether, data: "", - reverted: false + reverted: false, + storageAccesses: new Vm.StorageAccess[](0) }) ); assertEq( called[3], Vm.AccountAccess({ + accessor: address(this), account: b, kind: Vm.AccountAccessKind.Create, + oldBalance: 0, + newBalance: 0, + deployedCode: "", initialized: true, value: 1 ether, data: abi.encodePacked(type(SelfDestructor).creationCode, abi.encode(address(bytes20("doesn't exist yet")))), - reverted: false + reverted: false, + storageAccesses: new Vm.StorageAccess[](0) }) ); assertEq( called[4], Vm.AccountAccess({ + accessor: address(b), account: address(bytes20("doesn't exist yet")), kind: Vm.AccountAccessKind.SelfDestruct, + oldBalance: 0, + newBalance: 1 ether, + deployedCode: hex"", initialized: false, value: 1 ether, data: "", - reverted: false + reverted: false, + storageAccesses: new Vm.StorageAccess[](0) }) ); } function startRecordingFromLowerDepth() external { - cheats.recordAccountAccesses(); + cheats.recordStateDiff(); assembly { pop(call(gas(), 1234, 0, 0, 0, 0, 0)) } @@ -448,12 +843,65 @@ contract RecordAccountAccessesTest is DSTest { revert(); } + function assertIncrementEq( + Vm.StorageAccess memory read, + Vm.StorageAccess memory write, + Vm.StorageAccess memory expected + ) internal { + assertEq( + read, + Vm.StorageAccess({ + account: expected.account, + slot: expected.slot, + isWrite: false, + previousValue: expected.previousValue, + newValue: expected.previousValue, + reverted: expected.reverted + }) + ); + assertEq( + write, + Vm.StorageAccess({ + account: expected.account, + slot: expected.slot, + isWrite: true, + previousValue: expected.previousValue, + newValue: expected.newValue, + reverted: expected.reverted + }) + ); + } + function assertEq(Vm.AccountAccess memory actualAccess, Vm.AccountAccess memory expectedAccess) internal { + assertEq(actualAccess, expectedAccess, true); + } + + function assertEq(Vm.AccountAccess memory actualAccess, Vm.AccountAccess memory expectedAccess, bool checkStorage) internal { + assertEq(actualAccess.accessor, expectedAccess.accessor, "incorrect accessor"); assertEq(actualAccess.account, expectedAccess.account, "incorrect account"); assertEq(toUint(actualAccess.kind), toUint(expectedAccess.kind), "incorrect kind"); assertEq(toUint(actualAccess.initialized), toUint(expectedAccess.initialized), "incorrect initialized"); + assertEq(actualAccess.oldBalance, expectedAccess.oldBalance, "incorrect oldBalance"); + assertEq(actualAccess.newBalance, expectedAccess.newBalance, "incorrect newBalance"); + assertEq(actualAccess.deployedCode, expectedAccess.deployedCode, "incorrect deployedCode"); assertEq(actualAccess.value, expectedAccess.value, "incorrect value"); assertEq(actualAccess.data, expectedAccess.data, "incorrect data"); + assertEq(toUint(actualAccess.reverted), toUint(expectedAccess.reverted), "incorrect reverted"); + if (checkStorage) { + assertEq(actualAccess.storageAccesses.length, expectedAccess.storageAccesses.length, "incorrect storageAccesses length"); + for (uint256 i = 0; i < actualAccess.storageAccesses.length; i++) { + assertEq(actualAccess.storageAccesses[i], expectedAccess.storageAccesses[i]); + } + } + } + + function assertEq(Vm.StorageAccess memory actual, Vm.StorageAccess memory expected) internal { + assertEq(actual.account, expected.account, "incorrect storageAccess account"); + assertEq(actual.slot, expected.slot, "incorrect storageAccess slot"); + assertEq(toUint(actual.isWrite), toUint(expected.isWrite), "incorrect storageAccess isWrite"); + assertEq(actual.previousValue, expected.previousValue, "incorrect storageAccess previousValue"); + assertEq(actual.newValue, expected.newValue, "incorrect storageAccess newValue"); + assertEq(toUint(actual.reverted), toUint(expected.reverted), "incorrect storageAccess reverted"); } function toUint(Vm.AccountAccessKind kind) internal pure returns (uint256 value) { diff --git a/testdata/cheats/RecordStorageAccesses.t.sol b/testdata/cheats/RecordStorageAccesses.t.sol deleted file mode 100644 index 8677c40c1290..000000000000 --- a/testdata/cheats/RecordStorageAccesses.t.sol +++ /dev/null @@ -1,467 +0,0 @@ -// SPDX-License-Identifier: Unlicense -pragma solidity 0.8.18; - -import "ds-test/test.sol"; -import "./Vm.sol"; - -/// @notice Helper contract with a constructor that stores a value in storage -/// and then optionally reverts. -contract ConstructorStorer { - constructor(bool shouldRevert) { - assembly { - sstore(0x00, 0x01) - if shouldRevert { revert(0, 0) } - } - } -} - -/// @notice Helper contract that stores and reads in addition to calling a -/// function on itself (which also accesses storage) -contract Doer { - uint256[10] spacer; - mapping(bytes32 key => uint256 value) slots; - - constructor() { - slots[bytes32("doer 1")] = 10; - } - - function run() public { - slots[bytes32("doer 1")]++; - this.doStuff(); - } - - function doStuff() external { - slots[bytes32("doer 2")]++; - } -} - -/// @notice Helper contract that reverts after incrementing a storage slot. -contract Reverter { - Doer immutable doer; - mapping(bytes32 key => uint256 value) slots; - - constructor(Doer _doer) { - doer = _doer; - } - - function run() public { - doer.run(); - slots[bytes32("reverter")]++; - revert(); - } -} - -/// @notice Helper contract that increments a storage slot and then calls a -/// function on another contract (which also accesses storage) -contract Succeeder { - Doer immutable doer; - mapping(bytes32 key => uint256 value) slots; - - constructor(Doer _doer) { - doer = _doer; - } - - function run() public { - slots[bytes32("succeeder")]++; - doer.run(); - } -} - -/// @notice Helper dispatch contract that reverts if intended with Reverter -/// and calls Succeeder otherwise -contract NestedRunner { - Doer public immutable doer; - Reverter public immutable reverter; - Succeeder public immutable succeeder; - mapping(bytes32 key => uint256 value) slots; - - constructor() { - doer = new Doer(); - reverter = new Reverter(doer); - succeeder = new Succeeder(doer); - } - - function run(bool shouldRevert) public { - slots[bytes32("runner")]++; - try reverter.run() { - if (shouldRevert) { - revert(); - } - } catch {} - succeeder.run(); - if (shouldRevert) { - revert(); - } - } -} - -/// @notice Helper contract that directly reads from and writes to storage -contract StorageAccessor { - function read(bytes32 slot) public view returns (bytes32 value) { - assembly { - value := sload(slot) - } - } - - function write(bytes32 slot, bytes32 value) public { - assembly { - sstore(slot, value) - } - } -} - -/// @notice Helper contract that creates contracts with create2 -contract Create2or { - function create2(bytes32 salt, bytes memory initcode) external payable returns (address result) { - assembly { - result := create2(callvalue(), add(initcode, 0x20), mload(initcode), salt) - } - } -} - -/// @notice Test contract for recording storage accesses -contract RecordStorageAccessesTest is DSTest { - Create2or immutable create2or; - Vm constant cheats = Vm(HEVM_ADDRESS); - StorageAccessor test1; - StorageAccessor test2; - NestedRunner runner; - uint256 counter; - - constructor() { - create2or = new Create2or(); - } - - function setUp() public { - test1 = new StorageAccessor(); - test2 = new StorageAccessor(); - runner = new NestedRunner(); - counter = 5; - } - - /// @notice Test normal, non-nested storage accesses - function testRecordAccesses() public { - StorageAccessor one = test1; - StorageAccessor two = test2; - cheats.recordStorageAccesses(); - one.read(bytes32(uint256(1234))); - one.write(bytes32(uint256(1235)), bytes32(uint256(5678))); - two.write(bytes32(uint256(5678)), bytes32(uint256(123469))); - two.write(bytes32(uint256(5678)), bytes32(uint256(1234))); - - two.read(bytes32(uint256(5678))); - - Vm.StorageAccess[] memory accessed = cheats.getRecordedStorageAccesses(); - assertEq(accessed.length, 5, "incorrect length"); - Vm.StorageAccess memory access = accessed[0]; - assertEq( - access, - Vm.StorageAccess({ - account: address(one), - slot: bytes32(uint256(1234)), - isWrite: false, - previousValue: bytes32(uint256(0)), - newValue: bytes32(uint256(0)), - reverted: false - }) - ); - - access = accessed[1]; - assertEq( - access, - Vm.StorageAccess({ - account: address(one), - slot: bytes32(uint256(1235)), - isWrite: true, - previousValue: bytes32(uint256(0)), - newValue: bytes32(uint256(5678)), - reverted: false - }) - ); - - access = accessed[2]; - assertEq( - access, - Vm.StorageAccess({ - account: address(two), - slot: bytes32(uint256(5678)), - isWrite: true, - previousValue: bytes32(uint256(0)), - newValue: bytes32(uint256(123469)), - reverted: false - }) - ); - - access = accessed[3]; - assertEq( - access, - Vm.StorageAccess({ - account: address(two), - slot: bytes32(uint256(5678)), - isWrite: true, - previousValue: bytes32(uint256(123469)), - newValue: bytes32(uint256(1234)), - reverted: false - }) - ); - access = accessed[4]; - assertEq( - access, - Vm.StorageAccess({ - account: address(two), - slot: bytes32(uint256(5678)), - isWrite: false, - previousValue: bytes32(uint256(1234)), - newValue: bytes32(uint256(1234)), - reverted: false - }) - ); - } - - /// @notice Test storage access recordings with multiple nested calls, some - /// reverting, but overall successful. - function testNested() public { - cheats.recordStorageAccesses(); - runNested(false, false); - } - - /// @notice Test storage access recordings with multiple nested calls, some - /// reverting, with the first call reverting - function testNested_Revert() public { - cheats.recordStorageAccesses(); - - runNested(true, false); - } - - /// @notice Test that constructor storage accesses are recorded, including reverts - function testConstructorStorage() public { - cheats.recordStorageAccesses(); - address storer = address(new ConstructorStorer(false)); - try create2or.create2(bytes32(0), abi.encodePacked(type(ConstructorStorer).creationCode, abi.encode(true))) {} - catch {} - bytes memory creationCode = abi.encodePacked(type(ConstructorStorer).creationCode, abi.encode(true)); - - address hypotheticalStorer = deriveCreate2Address(address(create2or), bytes32(0), keccak256(creationCode)); - Vm.StorageAccess[] memory accessed = cheats.getRecordedStorageAccesses(); - assertEq(accessed.length, 2, "incorrect length"); - assertEq( - accessed[0], - Vm.StorageAccess({ - account: storer, - slot: bytes32(uint256(0)), - isWrite: true, - previousValue: bytes32(uint256(0)), - newValue: bytes32(uint256(1)), - reverted: false - }) - ); - assertEq( - accessed[1], - Vm.StorageAccess({ - account: hypotheticalStorer, - slot: bytes32(uint256(0)), - isWrite: true, - previousValue: bytes32(uint256(0)), - newValue: bytes32(uint256(1)), - reverted: true - }) - ); - } - - /// @notice Test that storage accesses are still recorded when the recording is started - /// from a lower call depth than the results are read from. - function testNested_LowerDepth() public { - this.startRecordingFromLowerDepth(); - runNested(false, true); - } - - /// @notice Test that storage accesses are still recorded when the recording is started - /// from a lower call depth than the results are read from, and the first call - /// reverts. - function testNested_LowerDepth_Revert() public { - this.startRecordingFromLowerDepth(); - runNested(true, true); - } - - function runNested(bool shouldRevert, bool expectFirst) internal { - try runner.run(shouldRevert) {} catch {} - Vm.StorageAccess[] memory accessed = cheats.getRecordedStorageAccesses(); - assertEq(accessed.length, 15 + toUint(expectFirst), "incorrect length"); - - uint256 startingIndex = toUint(expectFirst); - if (expectFirst) { - bytes32 counterSlot; - assembly { - counterSlot := counter.slot - } - assertEq( - accessed[0], - Vm.StorageAccess({ - account: address(this), - slot: counterSlot, - isWrite: false, - previousValue: bytes32(uint256(5)), - newValue: bytes32(uint256(5)), - reverted: false - }) - ); - } - bytes32 runnerSlot; - assembly { - runnerSlot := runner.slot - } - assertEq( - accessed[startingIndex], - Vm.StorageAccess({ - account: address(this), - slot: runnerSlot, - isWrite: false, - previousValue: bytes32(uint256(uint160(address(runner)))), - newValue: bytes32(uint256(uint160(address(runner)))), - reverted: false - }) - ); - - assertIncrementEq( - accessed[startingIndex + 1], - accessed[startingIndex + 2], - Vm.StorageAccess({ - account: address(runner), - slot: keccak256(abi.encodePacked(bytes32("runner"), bytes32(0))), - isWrite: true, - previousValue: bytes32(uint256(0)), - newValue: bytes32(uint256(1)), - reverted: shouldRevert - }) - ); - assertIncrementEq( - accessed[startingIndex + 3], - accessed[startingIndex + 4], - Vm.StorageAccess({ - account: address(runner.doer()), - slot: keccak256(abi.encodePacked(bytes32("doer 1"), uint256(10))), - isWrite: true, - previousValue: bytes32(uint256(10)), - newValue: bytes32(uint256(11)), - reverted: true - }) - ); - - assertIncrementEq( - accessed[startingIndex + 5], - accessed[startingIndex + 6], - Vm.StorageAccess({ - account: address(runner.doer()), - slot: keccak256(abi.encodePacked(bytes32("doer 2"), uint256(10))), - isWrite: true, - previousValue: bytes32(uint256(0)), - newValue: bytes32(uint256(1)), - reverted: true - }) - ); - - assertIncrementEq( - accessed[startingIndex + 7], - accessed[startingIndex + 8], - Vm.StorageAccess({ - account: address(runner.reverter()), - slot: keccak256(abi.encodePacked(bytes32("reverter"), uint256(0))), - isWrite: true, - previousValue: bytes32(uint256(0)), - newValue: bytes32(uint256(1)), - reverted: true - }) - ); - - assertIncrementEq( - accessed[startingIndex + 9], - accessed[startingIndex + 10], - Vm.StorageAccess({ - account: address(runner.succeeder()), - slot: keccak256(abi.encodePacked(bytes32("succeeder"), uint256(0))), - isWrite: true, - previousValue: bytes32(uint256(0)), - newValue: bytes32(uint256(1)), - reverted: shouldRevert - }) - ); - - Vm.StorageAccess memory expected = Vm.StorageAccess({ - account: address(runner.doer()), - slot: keccak256(abi.encodePacked(bytes32("doer 1"), uint256(10))), - isWrite: true, - previousValue: bytes32(uint256(10)), - newValue: bytes32(uint256(11)), - reverted: shouldRevert - }); - - assertIncrementEq(accessed[startingIndex + 11], accessed[startingIndex + 12], expected); - - assertIncrementEq( - accessed[startingIndex + 13], - accessed[startingIndex + 14], - Vm.StorageAccess({ - account: address(runner.doer()), - slot: keccak256(abi.encodePacked(bytes32("doer 2"), uint256(10))), - isWrite: true, - previousValue: bytes32(uint256(0)), - newValue: bytes32(uint256(1)), - reverted: shouldRevert - }) - ); - } - - function startRecordingFromLowerDepth() external returns (uint256 value) { - cheats.recordStorageAccesses(); - assembly { - // assign to a return value otherwise optimizer will remove - value := sload(counter.slot) - } - } - - function assertIncrementEq( - Vm.StorageAccess memory read, - Vm.StorageAccess memory write, - Vm.StorageAccess memory expected - ) internal { - assertEq( - read, - Vm.StorageAccess({ - account: expected.account, - slot: expected.slot, - isWrite: false, - previousValue: expected.previousValue, - newValue: expected.previousValue, - reverted: expected.reverted - }) - ); - assertEq( - write, - Vm.StorageAccess({ - account: expected.account, - slot: expected.slot, - isWrite: true, - previousValue: expected.previousValue, - newValue: expected.newValue, - reverted: expected.reverted - }) - ); - } - - function assertEq(Vm.StorageAccess memory actual, Vm.StorageAccess memory expected) internal { - assertEq(actual.account, expected.account, "incorrect account"); - assertEq(actual.slot, expected.slot, "incorrect slot"); - assertEq(toUint(actual.isWrite), toUint(expected.isWrite), "incorrect isWrite"); - assertEq(actual.previousValue, expected.previousValue, "incorrect previousValue"); - assertEq(actual.newValue, expected.newValue, "incorrect newValue"); - assertEq(toUint(actual.reverted), toUint(expected.reverted), "incorrect reverted"); - } - - function toUint(bool a) internal pure returns (uint256) { - return a ? 1 : 0; - } - - function deriveCreate2Address(address deployer, bytes32 salt, bytes32 codeHash) internal pure returns (address) { - return address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), deployer, salt, codeHash))))); - } -} diff --git a/testdata/cheats/Vm.sol b/testdata/cheats/Vm.sol index f1a7f1e18213..1b8e4a35ce34 100644 --- a/testdata/cheats/Vm.sol +++ b/testdata/cheats/Vm.sol @@ -15,7 +15,7 @@ interface Vm { struct FsMetadata { bool isDir; bool isSymlink; uint256 length; bool readOnly; uint256 modified; uint256 accessed; uint256 created; } struct Wallet { address addr; uint256 publicKeyX; uint256 publicKeyY; uint256 privateKey; } struct FfiResult { int32 exitCode; bytes stdout; bytes stderr; } - struct AccountAccess { address account; AccountAccessKind kind; bool initialized; uint256 value; bytes data; bool reverted; } + struct AccountAccess { address accessor; address account; AccountAccessKind kind; bool initialized; uint256 oldBalance; uint256 newBalance; bytes deployedCode; uint256 value; bytes data; bool reverted; StorageAccess[] storageAccesses; } struct StorageAccess { address account; bytes32 slot; bool isWrite; bytes32 previousValue; bytes32 newValue; bool reverted; } function accesses(address target) external returns (bytes32[] memory readSlots, bytes32[] memory writeSlots); function activeFork() external view returns (uint256 forkId); @@ -108,9 +108,8 @@ interface Vm { function getMappingSlotAt(address target, bytes32 mappingSlot, uint256 idx) external returns (bytes32 value); function getNonce(address account) external view returns (uint64 nonce); function getNonce(Wallet calldata wallet) external returns (uint64 nonce); - function getRecordedAccountAccesses() external returns (AccountAccess[] memory accesses); function getRecordedLogs() external returns (Log[] memory logs); - function getRecordedStorageAccesses() external returns (StorageAccess[] memory accesses); + function getStateDiff() external returns (AccountAccess[] memory accesses); function isDir(string calldata path) external returns (bool result); function isFile(string calldata path) external returns (bool result); function isPersistent(address account) external view returns (bool persistent); @@ -163,9 +162,8 @@ interface Vm { function readLine(string calldata path) external view returns (string memory line); function readLink(string calldata linkPath) external view returns (string memory targetPath); function record() external; - function recordAccountAccesses() external; function recordLogs() external; - function recordStorageAccesses() external; + function recordStateDiff() external; function rememberKey(uint256 privateKey) external returns (address keyAddr); function removeDir(string calldata path, bool recursive) external; function removeFile(string calldata path) external;