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