Skip to content

Commit d153812

Browse files
authored
feat(forge): filter through artifacts on invariant testing (#2635)
* filter through contract names or identifiers * use identifier method from ArtifactId * filter newly generated contracts * give a more helpful message * improve docs * fmt * rename Abi to Artifact * add in-run contract filtering test * fix fmt * add ContractsByArtifact and ContractsByArtifactExt * add ContractsByAddress * add ArtifactFilters * replace ContractsByArtifactExt * move some utils to common * add missing dev dependency * move override test from invariant tests * fix ArtifactFilters.get_targeted_functions
1 parent 5d6d065 commit d153812

32 files changed

+700
-191
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/src/cmd/forge/script/executor.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ impl ScriptArgs {
7676
transactions: VecDeque<TypedTransaction>,
7777
script_config: &mut ScriptConfig,
7878
decoder: &mut CallTraceDecoder,
79-
contracts: &BTreeMap<ArtifactId, (Abi, Vec<u8>)>,
79+
contracts: &ContractsByArtifact,
8080
) -> eyre::Result<VecDeque<TransactionWithMetadata>> {
8181
let mut runner = self
8282
.prepare_runner(script_config, script_config.evm_opts.sender, SimulationStage::OnChain)
@@ -93,8 +93,7 @@ impl ScriptArgs {
9393
.iter()
9494
.filter_map(|(addr, contract_id)| {
9595
let contract_name = utils::get_contract_name(contract_id);
96-
if let Some((_, (abi, _))) =
97-
contracts.iter().find(|(artifact, _)| artifact.name == contract_name)
96+
if let Ok(Some((_, (abi, _)))) = contracts.find_by_name_or_identifier(contract_name)
9897
{
9998
return Some((*addr, (contract_name.to_string(), abi)))
10099
}

cli/src/cmd/forge/script/mod.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ use forge::{
3030
CallTraceArena, CallTraceDecoder, CallTraceDecoderBuilder, TraceKind,
3131
},
3232
};
33-
use foundry_common::{evm::EvmArgs, CONTRACT_MAX_SIZE};
33+
use foundry_common::{evm::EvmArgs, ContractsByArtifact, CONTRACT_MAX_SIZE};
3434
use foundry_config::Config;
3535
use foundry_utils::{encode_args, format_token, IntoFunction};
3636
use serde::{Deserialize, Serialize};
@@ -161,7 +161,7 @@ impl ScriptArgs {
161161
&self,
162162
script_config: &ScriptConfig,
163163
result: &mut ScriptResult,
164-
known_contracts: &BTreeMap<ArtifactId, (Abi, Vec<u8>)>,
164+
known_contracts: &ContractsByArtifact,
165165
) -> eyre::Result<CallTraceDecoder> {
166166
let etherscan_identifier = EtherscanIdentifier::new(
167167
script_config.evm_opts.get_remote_chain_id(),
@@ -520,7 +520,7 @@ pub struct ScriptConfig {
520520
/// Data struct to help `ScriptSequence` verify contracts on `etherscan`.
521521
pub struct VerifyBundle {
522522
pub num_of_optimizations: Option<usize>,
523-
pub known_contracts: BTreeMap<ArtifactId, (Abi, Vec<u8>)>,
523+
pub known_contracts: ContractsByArtifact,
524524
pub etherscan_key: Option<String>,
525525
pub project_paths: ProjectPathsArgs,
526526
pub retry: RetryArgs,
@@ -530,7 +530,7 @@ impl VerifyBundle {
530530
pub fn new(
531531
project: &Project,
532532
config: &Config,
533-
known_contracts: BTreeMap<ArtifactId, (Abi, Vec<u8>)>,
533+
known_contracts: ContractsByArtifact,
534534
retry: RetryArgs,
535535
) -> Self {
536536
let num_of_optimizations =

cli/src/cmd/forge/script/sequence.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ impl ScriptSequence {
164164
if let (Some(contract_address), Some(data)) =
165165
(receipt.contract_address, tx.typed_tx().data())
166166
{
167-
for (artifact, (_contract, bytecode)) in &verify.known_contracts {
167+
for (artifact, (_contract, bytecode)) in verify.known_contracts.iter() {
168168
// If it's a CREATE2, the tx.data comes with a 32-byte salt in the beginning
169169
// of the transaction
170170
if data.0.split_at(create2_offset).1.starts_with(bytecode) {

cli/src/cmd/utils.rs

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use ethers::{
1414
},
1515
};
1616
use forge::executor::opts::EvmOpts;
17-
use foundry_common::TestFunctionExt;
17+
use foundry_common::{ContractsByArtifact, TestFunctionExt};
1818
use foundry_config::{figment::Figment, Chain as ConfigChain, Config};
1919
use foundry_utils::Retry;
2020
use std::{collections::BTreeMap, path::PathBuf};
@@ -169,22 +169,24 @@ pub fn needs_setup(abi: &Abi) -> bool {
169169
pub fn unwrap_contracts(
170170
contracts: &BTreeMap<ArtifactId, ContractBytecodeSome>,
171171
deployed_code: bool,
172-
) -> BTreeMap<ArtifactId, (Abi, Vec<u8>)> {
173-
contracts
174-
.iter()
175-
.filter_map(|(id, c)| {
176-
let bytecode = if deployed_code {
177-
c.deployed_bytecode.clone().into_bytes()
178-
} else {
179-
c.bytecode.clone().object.into_bytes()
180-
};
172+
) -> ContractsByArtifact {
173+
ContractsByArtifact(
174+
contracts
175+
.iter()
176+
.filter_map(|(id, c)| {
177+
let bytecode = if deployed_code {
178+
c.deployed_bytecode.clone().into_bytes()
179+
} else {
180+
c.bytecode.clone().object.into_bytes()
181+
};
181182

182-
if let Some(bytecode) = bytecode {
183-
return Some((id.clone(), (c.abi.clone(), bytecode.to_vec())))
184-
}
185-
None
186-
})
187-
.collect()
183+
if let Some(bytecode) = bytecode {
184+
return Some((id.clone(), (c.abi.clone(), bytecode.to_vec())))
185+
}
186+
None
187+
})
188+
.collect(),
189+
)
188190
}
189191

190192
#[macro_export]

common/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ foundry-config = { path = "../config" }
1515

1616
# eth
1717
ethers-core = { git = "https://github.com/gakonst/ethers-rs", default-features = false }
18+
ethers-solc = { git = "https://github.com/gakonst/ethers-rs", default-features = false }
1819
ethers-providers = { git = "https://github.com/gakonst/ethers-rs", default-features = false }
1920

2021
# io

common/src/contracts.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
//! commonly used contract types and functions
2+
3+
use ethers_core::{
4+
abi::{Abi, Event, Function},
5+
types::{Address, H256},
6+
};
7+
use ethers_solc::ArtifactId;
8+
use std::{
9+
collections::BTreeMap,
10+
ops::{Deref, DerefMut},
11+
};
12+
13+
type ArtifactWithContractRef<'a> = (&'a ArtifactId, &'a (Abi, Vec<u8>));
14+
15+
/// Wrapper type that maps an artifact to a contract ABI and bytecode.
16+
#[derive(Default)]
17+
pub struct ContractsByArtifact(pub BTreeMap<ArtifactId, (Abi, Vec<u8>)>);
18+
19+
impl ContractsByArtifact {
20+
/// Finds a contract which has a similar bytecode as `code`.
21+
pub fn find_by_code(&self, code: &[u8]) -> Option<ArtifactWithContractRef> {
22+
self.iter().find(|(_, (_, known_code))| diff_score(known_code, code) < 0.1)
23+
}
24+
/// Finds a contract which has the same contract name or identifier as `id`. If more than one is
25+
/// found, return error.
26+
pub fn find_by_name_or_identifier(
27+
&self,
28+
id: &str,
29+
) -> eyre::Result<Option<ArtifactWithContractRef>> {
30+
let contracts = self
31+
.iter()
32+
.filter(|(artifact, _)| artifact.name == id || artifact.identifier() == id)
33+
.collect::<Vec<_>>();
34+
35+
if contracts.len() > 1 {
36+
eyre::bail!("{id} has more than one implementation.");
37+
}
38+
39+
Ok(contracts.first().cloned())
40+
}
41+
42+
/// Flattens a group of contracts into maps of all events and functions
43+
pub fn flatten(&self) -> (BTreeMap<[u8; 4], Function>, BTreeMap<H256, Event>, Abi) {
44+
let flattened_funcs: BTreeMap<[u8; 4], Function> = self
45+
.iter()
46+
.flat_map(|(_name, (abi, _code))| {
47+
abi.functions()
48+
.map(|func| (func.short_signature(), func.clone()))
49+
.collect::<BTreeMap<[u8; 4], Function>>()
50+
})
51+
.collect();
52+
53+
let flattened_events: BTreeMap<H256, Event> = self
54+
.iter()
55+
.flat_map(|(_name, (abi, _code))| {
56+
abi.events()
57+
.map(|event| (event.signature(), event.clone()))
58+
.collect::<BTreeMap<H256, Event>>()
59+
})
60+
.collect();
61+
62+
// We need this for better revert decoding, and want it in abi form
63+
let mut errors_abi = Abi::default();
64+
self.iter().for_each(|(_name, (abi, _code))| {
65+
abi.errors().for_each(|error| {
66+
let entry =
67+
errors_abi.errors.entry(error.name.clone()).or_insert_with(Default::default);
68+
entry.push(error.clone());
69+
});
70+
});
71+
(flattened_funcs, flattened_events, errors_abi)
72+
}
73+
}
74+
75+
impl Deref for ContractsByArtifact {
76+
type Target = BTreeMap<ArtifactId, (Abi, Vec<u8>)>;
77+
78+
fn deref(&self) -> &Self::Target {
79+
&self.0
80+
}
81+
}
82+
83+
impl DerefMut for ContractsByArtifact {
84+
fn deref_mut(&mut self) -> &mut Self::Target {
85+
&mut self.0
86+
}
87+
}
88+
89+
/// Wrapper type that maps an address to a contract identifier and contract ABI.
90+
pub type ContractsByAddress = BTreeMap<Address, (String, Abi)>;
91+
92+
/// Very simple fuzzy matching of contract bytecode.
93+
///
94+
/// Will fail for small contracts that are essentially all immutable variables.
95+
pub fn diff_score(a: &[u8], b: &[u8]) -> f64 {
96+
let cutoff_len = usize::min(a.len(), b.len());
97+
if cutoff_len == 0 {
98+
return 1.0
99+
}
100+
101+
let a = &a[..cutoff_len];
102+
let b = &b[..cutoff_len];
103+
let mut diff_chars = 0;
104+
for i in 0..cutoff_len {
105+
if a[i] != b[i] {
106+
diff_chars += 1;
107+
}
108+
}
109+
diff_chars as f64 / cutoff_len as f64
110+
}

common/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
pub mod calc;
66
pub mod constants;
7+
pub mod contracts;
78
pub mod errors;
89
pub mod evm;
910
pub mod fmt;
@@ -12,4 +13,5 @@ pub mod provider;
1213
pub use provider::*;
1314
pub mod traits;
1415
pub use constants::*;
16+
pub use contracts::*;
1517
pub use traits::*;

0 commit comments

Comments
 (0)