Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 12 additions & 11 deletions Cargo.lock

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

53 changes: 20 additions & 33 deletions cast/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Cast
//!
//! Contains core function implementation for `cast`

use crate::rlp_converter::Item;
use base::{Base, NumberWithBase, ToBase};
use chrono::NaiveDateTime;
Expand All @@ -16,7 +17,7 @@ use ethers_core::{
parse_bytes32_string, parse_units, rlp, Units,
},
};
use ethers_etherscan::Client;
use ethers_etherscan::{errors::EtherscanError, Client};
use ethers_providers::{Middleware, PendingTransaction};
use eyre::{Context, Result};
use foundry_common::{abi::encode_args, fmt::*};
Expand Down Expand Up @@ -1254,45 +1255,37 @@ impl SimpleCast {
let client = Client::new(chain, api_key)?;

// get the source
let contract_source = match client.contract_source_code(address).await {
Ok(src) => src,
Err(ethers_etherscan::errors::EtherscanError::InvalidApiKey) => {
eyre::bail!("Invalid Etherscan API key. Did you set it correctly? You may be using an API key for another Etherscan API chain (e.g. Ethereum API key for Polygonscan).")
let source = match client.contract_source_code(address).await {
Ok(source) => source,
Err(EtherscanError::InvalidApiKey) => {
eyre::bail!("Invalid Etherscan API key. Did you set it correctly? You may be using an API key for another Etherscan API chain (e.g. Etherscan API key for Polygonscan).")
}
Err(EtherscanError::ContractCodeNotVerified(address)) => {
eyre::bail!("Contract source code at {:?} on {} not verified. Maybe you have selected the wrong chain?", address, chain)
}
Err(err) => {
eyre::bail!(err)
}
};

if contract_source
.items
.iter()
.any(|item| item.abi == "Contract source code not verified")
{
eyre::bail!("Contract source code at {:?} on {} not verified. Maybe you have selected the wrong chain?", address, chain)
}

let contract_source_names = contract_source
let names = source
.items
.iter()
.map(|item| item.contract_name.clone())
.collect::<Vec<String>>();

let mut abis = Vec::with_capacity(contract_source.items.len());
for item in &contract_source.items {
abis.push(serde_json::from_str(&item.abi)?);
}
// TODO: Abi to RawAbi ?
Copy link
Member Author

Choose a reason for hiding this comment

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

@mattsse not sure how to do this here, metadata's abi field is an Abi struct when we need a RawAbi, reasons explained in foundry_utils::abi::abi_to_solidity

Copy link
Member

Choose a reason for hiding this comment

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

ah sigh, we need to change this back to either string or RawAbi, because Abi is not lossless

Copy link
Member

@mattsse mattsse Sep 29, 2022

Choose a reason for hiding this comment

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

also suggesting that we move RawAbi to ethers-core

I'd change the abi field to String and add helper functions that return RawAbi and Abi, I think

let abis = source.abis().iter().cloned().map(|_| todo!()).collect();

(abis, contract_source_names)
(abis, names)
}
};
contract_abis
.iter()
.zip(&contract_names)
.map(|(contract_abi, contract_name)| {
let interface_source =
foundry_utils::abi::abi_to_solidity(contract_abi, contract_name)?;
Ok(InterfaceSource { name: contract_name.to_owned(), source: interface_source })
.zip(contract_names)
.map(|(contract_abi, name)| {
let source = foundry_utils::abi::abi_to_solidity(contract_abi, &name)?;
Ok(InterfaceSource { name, source })
})
.collect::<Result<Vec<InterfaceSource>>>()
}
Expand Down Expand Up @@ -1471,14 +1464,8 @@ impl SimpleCast {
etherscan_api_key: String,
) -> Result<String> {
let client = Client::new(chain, etherscan_api_key)?;
let meta = client.contract_source_code(contract_address.parse()?).await?;
let code = meta.source_code();

if code.is_empty() {
return Err(eyre::eyre!("unverified contract"))
}

Ok(code)
let metadata = client.contract_source_code(contract_address.parse()?).await?;
Ok(metadata.source_code())
}

/// Fetches the source code of verified contracts from etherscan and expands the resulting
Expand All @@ -1504,7 +1491,7 @@ impl SimpleCast {
) -> eyre::Result<()> {
let client = Client::new(chain, etherscan_api_key)?;
let meta = client.contract_source_code(contract_address.parse()?).await?;
let source_tree = meta.source_tree()?;
let source_tree = meta.source_tree();
source_tree.write_to(&output_directory)?;
Ok(())
}
Expand Down
2 changes: 1 addition & 1 deletion cli/src/cmd/forge/verify/etherscan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ use cast::SimpleCast;
use ethers::{
abi::Function,
etherscan::{
contract::{CodeFormat, VerifyContract},
utils::lookup_compiler_version,
verify::{CodeFormat, VerifyContract},
Client,
},
prelude::artifacts::StandardJsonCompilerInput,
Expand Down
2 changes: 1 addition & 1 deletion common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ foundry-config = { path = "../config" }
ethers-core = { git = "https://github.com/gakonst/ethers-rs", default-features = false }
ethers-solc = { git = "https://github.com/gakonst/ethers-rs", default-features = false }
ethers-providers = { git = "https://github.com/gakonst/ethers-rs", default-features = false }
ethers-etherscan = { git = "https://github.com/gakonst/ethers-rs", default-features = false }
ethers-etherscan = { git = "https://github.com/gakonst/ethers-rs", default-features = false, features = ["ethers-solc"] }

# io
reqwest = { version = "0.11", default-features = false }
Expand Down
87 changes: 58 additions & 29 deletions common/src/abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ use ethers_core::{
types::{Address, Chain, I256, U256},
utils::{hex, to_checksum},
};
use ethers_etherscan::Client;
use eyre::WrapErr;
use std::str::FromStr;
use ethers_etherscan::{contract::ContractMetadata, errors::EtherscanError, Client};
use eyre::{ContextCompat, Result, WrapErr};
use std::{future::Future, pin::Pin, str::FromStr};

/// Given a function and a vector of string arguments, it proceeds to convert the args to ethabi
/// Tokens and then ABI encode them.
pub fn encode_args(func: &Function, args: &[impl AsRef<str>]) -> eyre::Result<Vec<u8>> {
pub fn encode_args(func: &Function, args: &[impl AsRef<str>]) -> Result<Vec<u8>> {
let params = func
.inputs
.iter()
Expand All @@ -30,7 +30,7 @@ pub fn encode_args(func: &Function, args: &[impl AsRef<str>]) -> eyre::Result<Ve
/// # Panics
///
/// If the `sig` is an invalid function signature
pub fn abi_decode(sig: &str, calldata: &str, input: bool) -> eyre::Result<Vec<Token>> {
pub fn abi_decode(sig: &str, calldata: &str, input: bool) -> Result<Vec<Token>> {
let func = IntoFunction::into(sig);
let calldata = calldata.strip_prefix("0x").unwrap_or(calldata);
let calldata = hex::decode(calldata)?;
Expand All @@ -53,7 +53,7 @@ pub fn abi_decode(sig: &str, calldata: &str, input: bool) -> eyre::Result<Vec<To
pub fn parse_tokens<'a, I: IntoIterator<Item = (&'a ParamType, &'a str)>>(
params: I,
lenient: bool,
) -> eyre::Result<Vec<Token>> {
) -> Result<Vec<Token>> {
params
.into_iter()
.map(|(param, value)| {
Expand Down Expand Up @@ -96,26 +96,28 @@ pub fn parse_tokens<'a, I: IntoIterator<Item = (&'a ParamType, &'a str)>>(
.wrap_err("Failed to parse tokens")
}

/// cleans up potential shortcomings of the ethabi Tokenizer
/// Cleans up potential shortcomings of the ethabi Tokenizer.
///
/// For example: parsing a string array with a single empty string: `[""]`, is returned as
///
/// ```text
/// [
// String(
// "\"\"",
// ),
// ],
/// String(
/// "\"\"",
/// ),
/// ],
/// ```
///
///
/// But should just be
///
/// ```text
/// [
// String(
// "",
// ),
// ],
/// String(
/// "",
/// ),
/// ],
/// ```
///
///
/// This will handle this edge case
pub fn sanitize_token(token: Token) -> Token {
match token {
Expand Down Expand Up @@ -203,7 +205,7 @@ impl<'a> IntoFunction for &'a str {
}

/// Given a function signature string, it tries to parse it as a `Function`
pub fn get_func(sig: &str) -> eyre::Result<Function> {
pub fn get_func(sig: &str) -> Result<Function> {
Ok(match HumanReadableParser::parse_function(sig) {
Ok(func) => func,
Err(err) => {
Expand All @@ -225,7 +227,7 @@ pub fn get_func(sig: &str) -> eyre::Result<Function> {
}

/// Given an event signature string, it tries to parse it as a `Event`
pub fn get_event(sig: &str) -> eyre::Result<Event> {
pub fn get_event(sig: &str) -> Result<Event> {
Ok(HumanReadableParser::parse_event(sig)?)
}

Expand Down Expand Up @@ -260,19 +262,13 @@ pub async fn get_func_etherscan(
args: &[String],
chain: Chain,
etherscan_api_key: &str,
) -> eyre::Result<Function> {
) -> Result<Function> {
let client = Client::new(chain, etherscan_api_key)?;
let metadata = &client.contract_source_code(contract).await?.items[0];

let abi = if metadata.implementation.is_empty() {
serde_json::from_str(&metadata.abi)?
} else {
let implementation = metadata.implementation.parse::<Address>()?;
client.contract_abi(implementation).await?
};
let source = find_source(client, contract).await?;
let metadata = source.items.first().wrap_err("etherscan returned empty metadata")?;

let empty = vec![];
let funcs = abi.functions.get(function_name).unwrap_or(&empty);
let funcs = metadata.abi.functions.get(function_name).unwrap_or(&empty);

for func in funcs {
let res = encode_args(func, args);
Expand All @@ -284,6 +280,39 @@ pub async fn get_func_etherscan(
Err(eyre::eyre!("Function not found in abi"))
}

/// If the code at `address` is a proxy, recurse until we find the implementation.
pub fn find_source(
client: Client,
address: Address,
) -> Pin<Box<dyn Future<Output = Result<ContractMetadata>>>> {
Box::pin(async move {
tracing::trace!("find etherscan source for: {:?}", address);
let source = client.contract_source_code(address).await?;
let metadata = source.items.first().wrap_err("Etherscan returned no data")?;
if metadata.proxy == 0 {
Ok(source)
} else {
let implementation = metadata.implementation.unwrap();
println!(
"Contract at {} is a proxy, trying to fetch source at {:?}...",
address, implementation
);
match find_source(client, implementation).await {
impl_source @ Ok(_) => impl_source,
Err(e) => {
let err = EtherscanError::ContractCodeNotVerified(address).to_string();
if e.to_string() == err {
tracing::error!("{}", err);
Ok(source)
} else {
Err(e)
}
}
}
}
})
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading