diff --git a/assets/test_metadata.json b/assets/test_metadata.json index 7fca971..127e808 100644 --- a/assets/test_metadata.json +++ b/assets/test_metadata.json @@ -12,6 +12,14 @@ "address": "0xdeadbeef00000000000000000000000000000042", "expected_balance": "1233" }, + { + "address": "0xdeadbeef00000000000000000000000000000042", + "is_storage_empty": true + }, + { + "address": "0xdeadbeef00000000000000000000000000000042", + "is_storage_empty": false + }, { "instance": "WBTC_1", "method": "#deployer", diff --git a/crates/core/src/driver/mod.rs b/crates/core/src/driver/mod.rs index 32b13c5..ae96a08 100644 --- a/crates/core/src/driver/mod.rs +++ b/crates/core/src/driver/mod.rs @@ -4,6 +4,7 @@ use std::collections::HashMap; use std::marker::PhantomData; use std::path::PathBuf; +use alloy::consensus::EMPTY_ROOT_HASH; use alloy::hex; use alloy::json_abi::JsonAbi; use alloy::network::{Ethereum, TransactionBuilder}; @@ -28,6 +29,7 @@ use semver::Version; use revive_dt_format::case::{Case, CaseIdx}; use revive_dt_format::input::{ BalanceAssertion, Calldata, EtherValue, Expected, ExpectedOutput, Input, Method, + StorageEmptyAssertion, }; use revive_dt_format::metadata::{ContractInstance, ContractPathAndIdent}; use revive_dt_format::{input::Step, metadata::Metadata}; @@ -90,6 +92,11 @@ where .await?; Ok(StepOutput::BalanceAssertion) } + Step::StorageEmptyAssertion(storage_empty) => { + self.handle_storage_empty(metadata, case_idx, storage_empty, node) + .await?; + Ok(StepOutput::StorageEmptyAssertion) + } } } @@ -125,7 +132,22 @@ where ) -> anyhow::Result<()> { self.handle_balance_assertion_contract_deployment(metadata, balance_assertion, node) .await?; + self.handle_balance_assertion_execution(balance_assertion, node) + .await?; + Ok(()) + } + pub async fn handle_storage_empty( + &mut self, + metadata: &Metadata, + _: CaseIdx, + storage_empty: &StorageEmptyAssertion, + node: &T::Blockchain, + ) -> anyhow::Result<()> { + self.handle_storage_empty_assertion_contract_deployment(metadata, storage_empty, node) + .await?; + self.handle_storage_empty_assertion_execution(storage_empty, node) + .await?; Ok(()) } @@ -557,6 +579,67 @@ where Ok(()) } + pub async fn handle_storage_empty_assertion_contract_deployment( + &mut self, + metadata: &Metadata, + storage_empty_assertion: &StorageEmptyAssertion, + node: &T::Blockchain, + ) -> anyhow::Result<()> { + let Some(instance) = storage_empty_assertion + .address + .strip_prefix(".address") + .map(ContractInstance::new) + else { + return Ok(()); + }; + self.get_or_deploy_contract_instance( + &instance, + metadata, + Input::default_caller(), + None, + None, + node, + ) + .await?; + Ok(()) + } + + pub async fn handle_storage_empty_assertion_execution( + &mut self, + StorageEmptyAssertion { + address: address_string, + is_storage_empty, + }: &StorageEmptyAssertion, + node: &T::Blockchain, + ) -> anyhow::Result<()> { + let address = Address::from_slice( + Calldata::new_compound([address_string]) + .calldata(node, self.default_resolution_context()) + .await? + .get(12..32) + .expect("Can't fail"), + ); + + let storage = node.latest_state_proof(address, Default::default()).await?; + let is_empty = storage.storage_hash == EMPTY_ROOT_HASH; + + let expected = is_storage_empty; + let actual = is_empty; + + if *expected != actual { + tracing::error!(%expected, %actual, %address, "Storage Empty Assertion failed"); + anyhow::bail!( + "Storage Empty Assertion failed - Expected {} but got {} for {} resolved to {}", + expected, + actual, + address_string, + address, + ) + }; + + Ok(()) + } + /// Gets the information of a deployed contract or library from the state. If it's found to not /// be deployed then it will be deployed. /// @@ -780,6 +863,7 @@ where } } (StepOutput::BalanceAssertion, StepOutput::BalanceAssertion) => {} + (StepOutput::StorageEmptyAssertion, StepOutput::StorageEmptyAssertion) => {} _ => unreachable!("The two step outputs can not be of a different kind"), } @@ -795,4 +879,5 @@ where pub enum StepOutput { FunctionCall(TransactionReceipt, GethTrace, DiffMode), BalanceAssertion, + StorageEmptyAssertion, } diff --git a/crates/core/src/main.rs b/crates/core/src/main.rs index 70512a5..7336542 100644 --- a/crates/core/src/main.rs +++ b/crates/core/src/main.rs @@ -451,6 +451,7 @@ where .filter_map(|step| match step { Step::FunctionCall(input) => Some(input.caller), Step::BalanceAssertion(..) => None, + Step::StorageEmptyAssertion(..) => None, }) .next() .unwrap_or(Input::default_caller()); diff --git a/crates/format/src/input.rs b/crates/format/src/input.rs index cc96341..d742f3a 100644 --- a/crates/format/src/input.rs +++ b/crates/format/src/input.rs @@ -28,6 +28,8 @@ pub enum Step { FunctionCall(Box), /// A step for performing a balance assertion on some account or contract. BalanceAssertion(Box), + /// A step for asserting that the storage of some contract or account is empty. + StorageEmptyAssertion(Box), } #[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)] @@ -60,6 +62,20 @@ pub struct BalanceAssertion { pub expected_balance: U256, } +#[derive(Clone, Debug, Default, Serialize, Deserialize, Eq, PartialEq)] +pub struct StorageEmptyAssertion { + /// The address that the balance assertion should be done on. + /// + /// This is a string which will be resolved into an address when being processed. Therefore, + /// this could be a normal hex address, a variable such as `Test.address`, or perhaps even a + /// full on variable like `$VARIABLE:Uniswap`. It follows the same resolution rules that are + /// followed in the calldata. + pub address: String, + + /// A boolean of whether the storage of the address is empty or not. + pub is_storage_empty: bool, +} + #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] #[serde(untagged)] pub enum Expected { diff --git a/crates/node-interaction/src/lib.rs b/crates/node-interaction/src/lib.rs index b052c7a..a6e3b38 100644 --- a/crates/node-interaction/src/lib.rs +++ b/crates/node-interaction/src/lib.rs @@ -1,8 +1,8 @@ //! This crate implements all node interactions. -use alloy::primitives::{Address, U256}; +use alloy::primitives::{Address, StorageKey, U256}; use alloy::rpc::types::trace::geth::{DiffMode, GethDebugTracingOptions, GethTrace}; -use alloy::rpc::types::{TransactionReceipt, TransactionRequest}; +use alloy::rpc::types::{EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest}; use anyhow::Result; /// An interface for all interactions with Ethereum compatible nodes. @@ -25,4 +25,11 @@ pub trait EthereumNode { /// Returns the balance of the provided [`Address`] back. fn balance_of(&self, address: Address) -> impl Future>; + + /// Returns the latest storage proof of the provided [`Address`] + fn latest_state_proof( + &self, + address: Address, + keys: Vec, + ) -> impl Future>; } diff --git a/crates/node/src/geth.rs b/crates/node/src/geth.rs index b3618ba..38fba17 100644 --- a/crates/node/src/geth.rs +++ b/crates/node/src/geth.rs @@ -17,14 +17,16 @@ use alloy::{ eips::BlockNumberOrTag, genesis::{Genesis, GenesisAccount}, network::{Ethereum, EthereumWallet, NetworkWallet}, - primitives::{Address, BlockHash, BlockNumber, BlockTimestamp, FixedBytes, TxHash, U256}, + primitives::{ + Address, BlockHash, BlockNumber, BlockTimestamp, FixedBytes, StorageKey, TxHash, U256, + }, providers::{ Provider, ProviderBuilder, ext::DebugApi, fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller}, }, rpc::types::{ - TransactionReceipt, TransactionRequest, + EIP1186AccountProofResponse, TransactionReceipt, TransactionRequest, trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame}, }, signers::local::PrivateKeySigner, @@ -380,6 +382,20 @@ impl EthereumNode for GethNode { .await .map_err(Into::into) } + + #[tracing::instrument(skip_all, fields(geth_node_id = self.id))] + async fn latest_state_proof( + &self, + address: Address, + keys: Vec, + ) -> anyhow::Result { + self.provider() + .await? + .get_proof(address, keys) + .latest() + .await + .map_err(Into::into) + } } impl ResolverApi for GethNode { diff --git a/crates/node/src/kitchensink.rs b/crates/node/src/kitchensink.rs index 805b71f..4ef398c 100644 --- a/crates/node/src/kitchensink.rs +++ b/crates/node/src/kitchensink.rs @@ -17,7 +17,7 @@ use alloy::{ }, primitives::{ Address, B64, B256, BlockHash, BlockNumber, BlockTimestamp, Bloom, Bytes, FixedBytes, - TxHash, U256, + StorageKey, TxHash, U256, }, providers::{ Provider, ProviderBuilder, @@ -25,7 +25,7 @@ use alloy::{ fillers::{CachedNonceManager, ChainIdFiller, FillProvider, NonceFiller, TxFiller}, }, rpc::types::{ - TransactionReceipt, + EIP1186AccountProofResponse, TransactionReceipt, eth::{Block, Header, Transaction}, trace::geth::{DiffMode, GethDebugTracingOptions, PreStateConfig, PreStateFrame}, }, @@ -437,6 +437,20 @@ impl EthereumNode for KitchensinkNode { .await .map_err(Into::into) } + + #[tracing::instrument(skip_all, fields(kitchensink_node_id = self.id))] + async fn latest_state_proof( + &self, + address: Address, + keys: Vec, + ) -> anyhow::Result { + self.provider() + .await? + .get_proof(address, keys) + .latest() + .await + .map_err(Into::into) + } } impl ResolverApi for KitchensinkNode {