Skip to content

Commit 37e0199

Browse files
committed
feat(cheatcodes): add vm.getSelectors cheatcode
1 parent f084d2d commit 37e0199

5 files changed

Lines changed: 219 additions & 2 deletions

File tree

crates/cheatcodes/assets/cheatcodes.json

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

crates/cheatcodes/spec/src/vm.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1940,6 +1940,11 @@ interface Vm {
19401940
#[cheatcode(group = Filesystem)]
19411941
function getCode(string calldata artifactPath) external view returns (bytes memory creationBytecode);
19421942

1943+
/// Gets all function selectors from a contract artifact. Takes in the relative path to the json file or the path to the
1944+
/// artifact in the form of <path>:<contract>:<version> where <contract> and <version> parts are optional.
1945+
#[cheatcode(group = Filesystem)]
1946+
function getSelectors(string calldata artifactPath) external view returns (bytes4[] memory selectors);
1947+
19431948
/// Deploys a contract from an artifact file. Takes in the relative path to the json file or the path to the
19441949
/// artifact in the form of <path>:<contract>:<version> where <contract> and <version> parts are optional.
19451950
/// Reverts if the target artifact contains unlinked library placeholders.

crates/cheatcodes/src/fs.rs

Lines changed: 142 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ use crate::{
55
Cheatcode, Cheatcodes, CheatcodesExecutor, CheatsCtxt, Result, Vm::*, inspector::exec_create,
66
};
77
use alloy_dyn_abi::DynSolType;
8-
use alloy_json_abi::ContractObject;
8+
use alloy_json_abi::{ContractObject, JsonAbi};
99
use alloy_network::{Network, ReceiptResponse};
10-
use alloy_primitives::{Bytes, U256, hex, map::Entry};
10+
use alloy_primitives::{Bytes, FixedBytes, U256, hex, map::Entry};
1111
use alloy_sol_types::SolValue;
1212
use dialoguer::{Input, Password};
1313
use forge_script_sequence::{BroadcastReader, TransactionWithMetadata};
@@ -310,6 +310,16 @@ impl Cheatcode for getDeployedCodeCall {
310310
}
311311
}
312312

313+
impl Cheatcode for getSelectorsCall {
314+
fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
315+
let Self { artifactPath: path } = self;
316+
let abi = get_artifact_abi(state, path)?;
317+
let selectors: Vec<FixedBytes<4>> =
318+
abi.functions().map(|func| func.selector().into()).collect();
319+
Ok(selectors.abi_encode())
320+
}
321+
}
322+
313323
impl Cheatcode for deployCode_0Call {
314324
fn apply_full<FEN: FoundryEvmNetwork>(
315325
&self,
@@ -602,6 +612,136 @@ fn get_artifact_code<FEN: FoundryEvmNetwork>(
602612
maybe_bytecode.ok_or_else(|| fmt_err!("no bytecode for contract; is it abstract or unlinked?"))
603613
}
604614

615+
/// Returns the ABI of a matching artifact from the given path.
616+
///
617+
/// See [`get_artifact_code`] for the supported path formats.
618+
fn get_artifact_abi<FEN: FoundryEvmNetwork>(
619+
state: &Cheatcodes<FEN>,
620+
path: &str,
621+
) -> Result<JsonAbi> {
622+
if path.ends_with(".json") {
623+
// Read JSON artifact directly from disk.
624+
let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
625+
let data = fs::read_to_string(path)?;
626+
let json: serde_json::Value = serde_json::from_str(&data)?;
627+
let abi = json.get("abi").ok_or_else(|| fmt_err!("no `abi` field in artifact JSON"))?;
628+
return serde_json::from_value(abi.clone()).map_err(|e| fmt_err!("{e}"));
629+
}
630+
631+
let mut parts = path.split(':');
632+
633+
let mut file = None;
634+
let mut contract_name = None;
635+
let mut version = None;
636+
637+
let path_or_name = parts.next().unwrap();
638+
if path_or_name.contains('.') {
639+
file = Some(PathBuf::from(path_or_name));
640+
if let Some(name_or_version) = parts.next() {
641+
if name_or_version.contains('.') {
642+
version = Some(name_or_version);
643+
} else {
644+
contract_name = Some(name_or_version);
645+
version = parts.next();
646+
}
647+
}
648+
} else {
649+
contract_name = Some(path_or_name);
650+
version = parts.next();
651+
}
652+
653+
let version = if let Some(version) = version {
654+
Some(Version::parse(version).map_err(|e| fmt_err!("failed parsing version: {e}"))?)
655+
} else {
656+
None
657+
};
658+
659+
// Use available artifacts list if present.
660+
if let Some(artifacts) = &state.config.available_artifacts {
661+
let filtered = artifacts
662+
.iter()
663+
.filter(|(id, _)| {
664+
let id_name = id.name.split('.').next().unwrap();
665+
666+
if let Some(path) = &file
667+
&& !id.source.ends_with(path)
668+
{
669+
return false;
670+
}
671+
if let Some(name) = contract_name
672+
&& id_name != name
673+
{
674+
return false;
675+
}
676+
if let Some(ref version) = version
677+
&& (id.version.minor != version.minor
678+
|| id.version.major != version.major
679+
|| id.version.patch != version.patch)
680+
{
681+
return false;
682+
}
683+
true
684+
})
685+
.collect::<Vec<_>>();
686+
687+
let artifact = match &filtered[..] {
688+
[] => None,
689+
[artifact] => Some(Ok(*artifact)),
690+
filtered => {
691+
let mut filtered = filtered.to_vec();
692+
Some(
693+
state
694+
.config
695+
.running_artifact
696+
.as_ref()
697+
.and_then(|running| {
698+
filtered.retain(|(id, _)| id.version == running.version);
699+
if filtered.len() == 1 {
700+
return Some(filtered[0]);
701+
}
702+
filtered.retain(|(id, _)| id.profile == running.profile);
703+
(filtered.len() == 1).then(|| filtered[0])
704+
})
705+
.ok_or_else(|| fmt_err!("multiple matching artifacts found")),
706+
)
707+
}
708+
};
709+
710+
if let Some(artifact) = artifact {
711+
let artifact = artifact?;
712+
return Ok(artifact.1.abi.clone());
713+
}
714+
}
715+
716+
// Fallback: construct path manually and read JSON from disk.
717+
let path_in_artifacts = match (file.map(|f| f.to_string_lossy().to_string()), contract_name) {
718+
(Some(file), Some(contract_name)) => {
719+
PathBuf::from(format!("{file}/{contract_name}.json"))
720+
}
721+
(None, Some(contract_name)) => {
722+
PathBuf::from(format!("{contract_name}.sol/{contract_name}.json"))
723+
}
724+
(Some(file), None) => {
725+
let name = file.replace(".sol", "");
726+
PathBuf::from(format!("{file}/{name}.json"))
727+
}
728+
_ => bail!("invalid artifact path"),
729+
};
730+
731+
let path = state.config.paths.artifacts.join(path_in_artifacts);
732+
let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
733+
let data = fs::read_to_string(path).map_err(|e| {
734+
if state.config.available_artifacts.is_some() {
735+
fmt_err!("no matching artifact found")
736+
} else {
737+
e.into()
738+
}
739+
})?;
740+
let json: serde_json::Value = serde_json::from_str(&data)?;
741+
let abi = json.get("abi").ok_or_else(|| fmt_err!("no `abi` field in artifact JSON"))?;
742+
serde_json::from_value(abi.clone()).map_err(|e| fmt_err!("{e}"))
743+
}
744+
605745
impl Cheatcode for ffiCall {
606746
fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
607747
let Self { commandInput: input } = self;
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// SPDX-License-Identifier: MIT OR Apache-2.0
2+
pragma solidity =0.8.18;
3+
4+
import "utils/Test.sol";
5+
6+
contract TargetContract {
7+
function foo() external pure returns (uint256) {
8+
return 1;
9+
}
10+
11+
function bar(uint256 x) external pure returns (uint256) {
12+
return x;
13+
}
14+
15+
function baz(address a, uint256 b) external pure returns (bool) {
16+
return a != address(0) && b > 0;
17+
}
18+
}
19+
20+
contract GetSelectorsTest is Test {
21+
function testGetSelectorsByName() public {
22+
bytes4[] memory selectors = vm.getSelectors("TargetContract");
23+
assertEq(selectors.length, 3, "should return 3 selectors");
24+
25+
// Verify each known selector is present.
26+
bytes4 fooSel = TargetContract.foo.selector;
27+
bytes4 barSel = TargetContract.bar.selector;
28+
bytes4 bazSel = TargetContract.baz.selector;
29+
30+
assertTrue(_contains(selectors, fooSel), "should contain foo selector");
31+
assertTrue(_contains(selectors, barSel), "should contain bar selector");
32+
assertTrue(_contains(selectors, bazSel), "should contain baz selector");
33+
}
34+
35+
function testGetSelectorsByNameAndVersion() public {
36+
bytes4[] memory selectors = vm.getSelectors("TargetContract:0.8.18");
37+
assertEq(selectors.length, 3, "should return 3 selectors");
38+
}
39+
40+
function testGetSelectorsByFullPath() public {
41+
bytes4[] memory selectors = vm.getSelectors("cheats/GetSelectors.t.sol:TargetContract");
42+
assertEq(selectors.length, 3, "should return 3 selectors");
43+
}
44+
45+
function _contains(bytes4[] memory arr, bytes4 val) internal pure returns (bool) {
46+
for (uint256 i = 0; i < arr.length; i++) {
47+
if (arr[i] == val) return true;
48+
}
49+
return false;
50+
}
51+
}

testdata/utils/Vm.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@ interface Vm {
323323
function getRawBlockHeader(uint256 blockNumber) external view returns (bytes memory rlpHeader);
324324
function getRecordedLogs() external view returns (Log[] memory logs);
325325
function getRecordedLogsJson() external view returns (string memory logsJson);
326+
function getSelectors(string calldata artifactPath) external view returns (bytes4[] memory selectors);
326327
function getStateDiff() external view returns (string memory diff);
327328
function getStateDiffJson() external view returns (string memory diff);
328329
function getStorageAccesses() external view returns (StorageAccess[] memory storageAccesses);

0 commit comments

Comments
 (0)