Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
386c642
create separate args
DaniPopes Sep 24, 2022
f50e3e1
feat: storage, initial
DaniPopes Sep 24, 2022
a3026da
use tmp dir
DaniPopes Sep 24, 2022
76afbd1
refactor: parsing, errors, cleanup
DaniPopes Sep 24, 2022
a91190f
feat: resolve proxy implementations
DaniPopes Sep 24, 2022
676a536
chore: clippy
DaniPopes Sep 24, 2022
6cd4d36
Merge branch 'master' into feat/cast-storage
DaniPopes Sep 24, 2022
56d2edd
chore: fmt
DaniPopes Sep 24, 2022
f3a6a60
chore: clippy
DaniPopes Sep 24, 2022
9b048e8
fix: output selection
DaniPopes Sep 24, 2022
209d2e1
feat: add warnings
DaniPopes Sep 24, 2022
31df1ef
wip
DaniPopes Sep 28, 2022
bcd954d
merge master
DaniPopes Sep 28, 2022
0f1fdda
update Cargo.lock
DaniPopes Sep 28, 2022
c6d270b
merge master
DaniPopes Sep 29, 2022
2a02f8d
fix
DaniPopes Sep 29, 2022
f7800f2
chore: clippy
DaniPopes Sep 29, 2022
6421482
wip
DaniPopes Oct 1, 2022
709584b
Merge branch 'master' into feat/cast-storage
DaniPopes Oct 1, 2022
12cffac
Merge branch 'master' into feat/cast-storage
DaniPopes Oct 1, 2022
8c7edbd
chore: update arg parsing, clean up debugging
DaniPopes Oct 1, 2022
06a2ae9
Merge remote-tracking branch 'upstream/master' into feat/cast-storage
DaniPopes Oct 14, 2022
d6aadf3
fix: rpc url
DaniPopes Oct 14, 2022
b767bc5
feat: try recompile with newer solc
DaniPopes Oct 14, 2022
bb7d4ab
Merge branch 'master' into feat/cast-storage
DaniPopes Oct 14, 2022
a97aeee
add test
DaniPopes Oct 14, 2022
65dc8cb
feat: add initial storage fetcher
DaniPopes Oct 14, 2022
40ac1e2
fix test
DaniPopes Nov 16, 2022
f25f73a
Merge branch 'master' into feat/cast-storage
DaniPopes Nov 16, 2022
3aa63e3
update TODOs
DaniPopes Nov 16, 2022
6214e69
other fixes
DaniPopes Nov 16, 2022
d6c335f
Merge branch 'master' into feat/cast-storage
DaniPopes Nov 22, 2022
3d16d55
ci: use etherscan api key in all tests
gakonst Dec 18, 2022
d72ab60
Revert "ci: use etherscan api key in all tests"
gakonst Dec 18, 2022
607d829
fix: add test to live ci
DaniPopes Dec 18, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .github/workflows/live-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,17 @@ jobs:
TEST_PRIVATE_KEY: ${{ secrets.TEST_PRIVATE_KEY }}
steps:
- name: Checkout sources
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
- name: Install nextest
uses: taiki-e/install-action@nextest
- uses: Swatinem/rust-cache@v1
with:
cache-on-failure: true

- name: cargo test
run: cargo test --package foundry-cli --test it -- verify::test_live_can_deploy_and_verify --exact --nocapture
- name: cargo nextest
run: cargo nextest run -p foundry-cli -E "!test(~fork) & test(~live)"
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions cast/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,35 @@ where
let res = self.provider.provider().request::<T, serde_json::Value>(method, params).await?;
Ok(serde_json::to_string(&res)?)
}

/// Returns the slot
///
/// # Example
///
/// ```no_run
/// use cast::Cast;
/// use ethers_providers::{Provider, Http};
/// use ethers_core::types::{Address, H256};
/// use std::{str::FromStr, convert::TryFrom};
///
/// # async fn foo() -> eyre::Result<()> {
/// let provider = Provider::<Http>::try_from("http://localhost:8545")?;
/// let cast = Cast::new(provider);
/// let addr = Address::from_str("0x00000000006c3852cbEf3e08E8dF289169EdE581")?;
/// let slot = H256::zero();
/// let storage = cast.storage(addr, slot, None).await?;
/// println!("{}", storage);
/// # Ok(())
/// # }
/// ```
pub async fn storage<T: Into<NameOrAddress> + Send + Sync>(
&self,
from: T,
slot: H256,
block: Option<BlockId>,
) -> Result<String> {
Ok(format!("{:?}", self.provider.get_storage_at(from, slot, block).await?))
}
}

pub struct InterfaceSource {
Expand Down
1 change: 1 addition & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ dunce = "1.0.2"
glob = "0.3.0"
globset = "0.4.8"
path-slash = "0.2.0"
tempfile = "3.3.0"

# misc
eyre = "0.6"
Expand Down
8 changes: 1 addition & 7 deletions cli/src/cast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,13 +295,7 @@ async fn main() -> eyre::Result<()> {
println!("{}", serde_json::to_string(&value)?);
}
Subcommands::Rpc(cmd) => cmd.run()?.await?,
Subcommands::Storage { address, slot, rpc_url, block } => {
let rpc_url = try_consume_config_rpc_url(rpc_url)?;

let provider = try_get_http_provider(rpc_url)?;
let value = provider.get_storage_at(address, slot, block).await?;
println!("{value:?}");
}
Subcommands::Storage(cmd) => cmd.run().await?,

// Calls & transactions
Subcommands::Call(cmd) => cmd.run().await?,
Expand Down
1 change: 1 addition & 0 deletions cli/src/cmd/cast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ pub mod interface;
pub mod rpc;
pub mod run;
pub mod send;
pub mod storage;
pub mod wallet;
256 changes: 256 additions & 0 deletions cli/src/cmd/cast/storage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
use crate::{
cmd::forge::build,
opts::cast::{parse_block_id, parse_name_or_address, parse_slot},
utils::try_consume_config_rpc_url,
};
use cast::Cast;
use clap::Parser;
use comfy_table::Table;
use ethers::{
abi::ethabi::ethereum_types::BigEndianHash, etherscan::Client, prelude::*,
solc::artifacts::StorageLayout,
};
use eyre::{ContextCompat, Result};
use foundry_common::{
abi::find_source,
compile::{compile, etherscan_project, suppress_compile},
try_get_http_provider, RetryProvider,
};
use foundry_config::Config;
use futures::future::join_all;
use semver::Version;

/// The minimum Solc version for outputting storage layouts.
///
/// https://github.com/ethereum/solidity/blob/develop/Changelog.md#065-2020-04-06
const MIN_SOLC: Version = Version::new(0, 6, 5);

#[derive(Debug, Clone, Parser)]
pub struct StorageArgs {
// Storage
#[clap(help = "The contract address.", value_parser = parse_name_or_address, value_name = "ADDRESS")]
address: NameOrAddress,
#[clap(
help = "The storage slot number (hex or decimal)",
value_parser = parse_slot,
value_name = "SLOT"
)]
slot: Option<H256>,
#[clap(long, env = "ETH_RPC_URL", value_name = "URL")]
rpc_url: Option<String>,
#[clap(
long,
short = 'B',
help = "The block height you want to query at.",
long_help = "The block height you want to query at. Can also be the tags earliest, latest, or pending.",
value_parser = parse_block_id,
value_name = "BLOCK"
)]
block: Option<BlockId>,

// Etherscan
#[clap(long, short, env = "ETHERSCAN_API_KEY", help = "etherscan API key", value_name = "KEY")]
etherscan_api_key: Option<String>,
#[clap(
long,
visible_alias = "chain-id",
env = "CHAIN",
help = "The chain ID the contract is deployed to.",
default_value = "mainnet",
value_name = "CHAIN"
)]
chain: Chain,

// Forge
#[clap(flatten)]
build: build::CoreBuildArgs,
}

impl StorageArgs {
pub async fn run(self) -> Result<()> {
let Self { address, block, build, rpc_url, slot, chain, etherscan_api_key } = self;

let rpc_url = try_consume_config_rpc_url(rpc_url)?;
let provider = try_get_http_provider(rpc_url)?;

let address = match address {
NameOrAddress::Name(name) => provider.resolve_name(&name).await?,
NameOrAddress::Address(address) => address,
};

// Slot was provided, perform a simple RPC call
if let Some(slot) = slot {
let cast = Cast::new(provider);
println!("{}", cast.storage(address, slot, block).await?);
return Ok(())
}

// No slot was provided
// Get deployed bytecode at given address
let address_code = provider.get_code(address, block).await?;
if address_code.is_empty() {
eyre::bail!("Provided address has no deployed code and thus no storage");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically if the contract selfdestructed, then it could have no code but have storage. I don't think there would be any way to get the storage reliably though, geth does have a RPC endpoint debug_storageRangeAt but its not part of the official json rpc spec

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, there's also parity_listStorageKeys, we could check if they are present and throw when they're not

}

// Check if we're in a forge project
let mut project = build.project()?;
if project.paths.has_input_files() {
// Find in artifacts and pretty print
add_storage_layout_output(&mut project);
let out = compile(&project, false, false)?;
let match_code = |artifact: &ConfigurableContractArtifact| -> Option<bool> {
let bytes =
artifact.deployed_bytecode.as_ref()?.bytecode.as_ref()?.object.as_bytes()?;
Some(bytes == &address_code)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the case of an external library, will the object be linked here? If it isn't then this match will always fail

};
let artifact =
out.artifacts().find(|(_, artifact)| match_code(artifact).unwrap_or_default());
if let Some((_, artifact)) = artifact {
return fetch_and_print_storage(provider, address, artifact, true).await
}
}

// Not a forge project or artifact not found
// Get code from Etherscan
println!("No matching artifacts found, fetching source code from Etherscan...");
let api_key = etherscan_api_key.or_else(|| {
let config = Config::load();
config.get_etherscan_api_key(Some(chain))
}).wrap_err("No Etherscan API Key is set. Consider using the ETHERSCAN_API_KEY env var, or setting the -e CLI argument or etherscan-api-key in foundry.toml")?;
let client = Client::new(chain, api_key)?;
let source = find_source(client, address).await?;
let metadata = source.items.first().unwrap();
if metadata.is_vyper() {
eyre::bail!("Contract at provided address is not a valid Solidity contract")
}

let version = metadata.compiler_version()?;
let auto_detect = version < MIN_SOLC;

// Create a new temp project
// TODO: Cache instead of using a temp directory: metadata from Etherscan won't change
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this isn't very important and we shouldn't cache this in .foundry imo

Copy link
Member Author

@DaniPopes DaniPopes Nov 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah its more so you dont have to download+compile every time when making multiple queries on the same address

let root = tempfile::tempdir()?;
let root_path = root.path();
let mut project = etherscan_project(metadata, root_path)?;
add_storage_layout_output(&mut project);
project.auto_detect = auto_detect;

// Compile
let mut out = suppress_compile(&project)?;
let artifact = {
let (_, mut artifact) = out
.artifacts()
.find(|(name, _)| name == &metadata.contract_name)
.ok_or_else(|| eyre::eyre!("Could not find artifact"))?;

if is_storage_layout_empty(&artifact.storage_layout) && auto_detect {
// try recompiling with the minimum version
println!("The requested contract was compiled with {version} while the minimum version for storage layouts is {MIN_SOLC} and as a result the output may be empty.");
let solc = Solc::find_or_install_svm_version(MIN_SOLC.to_string())?;
project.solc = solc;
project.auto_detect = false;
if let Ok(output) = suppress_compile(&project) {
out = output;
let (_, new_artifact) = out
.artifacts()
.find(|(name, _)| name == &metadata.contract_name)
.ok_or_else(|| eyre::eyre!("Could not find artifact"))?;
artifact = new_artifact;
}
}

artifact
};

// Clear temp directory
root.close()?;

fetch_and_print_storage(provider, address, artifact, true).await
}
}

async fn fetch_and_print_storage(
provider: RetryProvider,
address: Address,
artifact: &ConfigurableContractArtifact,
pretty: bool,
) -> Result<()> {
if is_storage_layout_empty(&artifact.storage_layout) {
println!("Storage layout is empty.");
Ok(())
} else {
let mut layout = artifact.storage_layout.as_ref().unwrap().clone();
fetch_storage_values(provider, address, &mut layout).await?;
print_storage(layout, pretty)
}
}

/// Overrides the `value` field in [StorageLayout] with the slot's value to avoid creating new data
/// structures.
async fn fetch_storage_values(
provider: RetryProvider,
address: Address,
layout: &mut StorageLayout,
) -> Result<()> {
// TODO: Batch request; handle array values;
let futures: Vec<_> = layout
.storage
.iter()
.map(|slot| {
let slot_h256 = H256::from_uint(&slot.slot.parse::<U256>()?);
Ok(provider.get_storage_at(address, slot_h256, None))
})
.collect::<Result<_>>()?;

for (value, slot) in join_all(futures).await.into_iter().zip(layout.storage.iter()) {
let value = value?.into_uint();
let t = layout.types.get_mut(&slot.storage_type).expect("Bad storage");
// TODO: Better format values according to their Solidity type
t.value = Some(format!("{value:?}"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we pretty format here or is can we remove the TODO?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure how to format a H256 into solidity values. im assuming we already do this somewhere in forge when displaying traces

}

Ok(())
}

fn print_storage(layout: StorageLayout, pretty: bool) -> Result<()> {
if !pretty {
println!("{}", serde_json::to_string_pretty(&serde_json::to_value(layout)?)?);
return Ok(())
}

let mut table = Table::new();
table.set_header(vec!["Name", "Type", "Slot", "Offset", "Bytes", "Value", "Contract"]);

for slot in layout.storage {
let storage_type = layout.types.get(&slot.storage_type);
table.add_row(vec![
slot.label,
storage_type.as_ref().map_or("?".to_string(), |t| t.label.clone()),
slot.slot,
slot.offset.to_string(),
storage_type.as_ref().map_or("?".to_string(), |t| t.number_of_bytes.clone()),
storage_type
.as_ref()
.map_or("?".to_string(), |t| t.value.clone().unwrap_or_else(|| "0".to_string())),
slot.contract,
]);
}

println!("{table}");

Ok(())
}

fn add_storage_layout_output(project: &mut Project) {
project.artifacts.additional_values.storage_layout = true;
let output_selection = project.artifacts.output_selection();
project.solc_config.settings.push_all(output_selection);
}

fn is_storage_layout_empty(storage_layout: &Option<StorageLayout>) -> bool {
if let Some(ref s) = storage_layout {
s.storage.is_empty()
} else {
true
}
}
Loading