Skip to content

feat(forge): eip712 cheatcodes + forge cmd (eip712 + bind-json) with solar #10510

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 50 commits into from
Jun 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
0a6982a
wip
0xrusowsky May 9, 2025
76fa60e
feat: eip712 type hash PoC
0xrusowsky May 9, 2025
4aeaceb
style: json
0xrusowsky May 9, 2025
d031f04
style: json
0xrusowsky May 9, 2025
078438b
style: json
0xrusowsky May 9, 2025
a9c5ea1
style: comments
0xrusowsky May 9, 2025
44fc6c3
wip
0xrusowsky May 12, 2025
2748946
initial impl using solar
0xrusowsky May 13, 2025
c752df6
fix: untracked change
0xrusowsky May 13, 2025
6cfc2f5
fix: optimize resolve_type
0xrusowsky May 13, 2025
027581c
initial working impl
0xrusowsky May 13, 2025
d1de66c
feat: eip712 solar resolver
0xrusowsky May 13, 2025
2ee28c6
style: docs + fmt + clippy
0xrusowsky May 13, 2025
d270789
todo: cheatcode
0xrusowsky May 13, 2025
b444a5b
docs: comments
0xrusowsky May 13, 2025
8f6bfcc
fix: use HIR rather than AST
0xrusowsky May 14, 2025
bb0d09b
from build opts
0xrusowsky May 14, 2025
864a0f4
docs
0xrusowsky May 14, 2025
491b7f0
fix: rmv hashset
0xrusowsky May 14, 2025
695d1ae
create utils for solar_pcx_from_build_opts
0xrusowsky May 14, 2025
85b8d31
incorporate version logic into `solar_pcx_from_build_opts`
0xrusowsky May 14, 2025
6d35756
wip bind-json: eip712 resolver integration
0xrusowsky May 15, 2025
00bb04d
forge(bind-json): integrate solar
0xrusowsky May 16, 2025
924516c
fix: tests
0xrusowsky May 16, 2025
c4872f1
Merge branch 'master' into feat/eip712-with-solar
0xrusowsky May 16, 2025
df74d85
style: clippy
0xrusowsky May 16, 2025
eeeb40e
undo cheatcode setup (will tackle it on its own PR)
0xrusowsky May 16, 2025
c259a17
rmv old test
0xrusowsky May 16, 2025
0f90d05
Merge branch 'master' into feat/eip712-with-solar
0xrusowsky May 19, 2025
f5e3697
style: fix typo
0xrusowsky May 19, 2025
d55b781
fix: win path
0xrusowsky May 19, 2025
452daca
Merge branch 'feat/eip712-with-solar' of github.com:foundry-rs/foundr…
0xrusowsky May 19, 2025
70a62ee
fix: merge conflicts
0xrusowsky May 19, 2025
86e174a
Merge branch 'master' into feat/eip712-with-solar
0xrusowsky May 19, 2025
7c7783a
Merge branch 'feat/eip712-with-solar' of github.com:foundry-rs/foundr…
0xrusowsky May 19, 2025
0bc5e4e
fix: dani's feedback
0xrusowsky May 19, 2025
5c1cf86
fix: merge conflict
0xrusowsky May 19, 2025
c5a0bc8
docs: explain bindings overriding
0xrusowsky May 19, 2025
eb63993
Merge branch 'master' into feat/eip712-with-solar
0xrusowsky May 19, 2025
ff1e857
Merge branch 'master' of github.com:foundry-rs/foundry into feat/eip7…
0xrusowsky May 20, 2025
6997528
Merge branch 'feat/eip712-with-solar' of github.com:foundry-rs/foundr…
0xrusowsky May 20, 2025
3a5abac
chore: patch solar
0xrusowsky May 20, 2025
9f0b62c
feat(forge): eip712 cheatcodes (#10570)
0xrusowsky May 29, 2025
10a8232
Merge branch 'master' of github.com:foundry-rs/foundry into feat/eip7…
0xrusowsky May 29, 2025
f299896
fix: bump solang parser
0xrusowsky May 29, 2025
5824d5f
Merge branch 'master' into eip712-solar
grandizzy Jun 3, 2025
bb363ea
Remove unused from forge crate
grandizzy Jun 3, 2025
1e77d63
Move tests to eip712
grandizzy Jun 3, 2025
c638ada
Nit: comments
grandizzy Jun 3, 2025
9e71342
Merge branch 'master' into feat/eip712-with-solar
grandizzy Jun 3, 2025
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
3 changes: 3 additions & 0 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion crates/anvil/core/src/eth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -664,7 +664,7 @@ pub enum EthRequest {

/// Add an address to the [`DelegationCapability`] of the wallet
///
/// [`DelegationCapability`]: wallet::DelegationCapability
/// [`DelegationCapability`]: wallet::DelegationCapability
#[serde(rename = "anvil_addCapability", with = "sequence")]
AnvilAddCapability(Address),

Expand Down
100 changes: 100 additions & 0 deletions crates/cheatcodes/assets/cheatcodes.json

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

49 changes: 49 additions & 0 deletions crates/cheatcodes/spec/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2888,6 +2888,55 @@ interface Vm {
/// catch (bytes memory interceptedInitcode) { initcode = interceptedInitcode; }
#[cheatcode(group = Utilities, safety = Unsafe)]
function interceptInitcode() external;

/// Generates the hash of the canonical EIP-712 type representation.
///
/// Supports 2 different inputs:
/// 1. Name of the type (i.e. "Transaction"):
/// * requires previous binding generation with `forge bind-json`.
/// * bindings will be retrieved from the path configured in `foundry.toml`.
///
/// 2. String representation of the type (i.e. "Foo(Bar bar) Bar(uint256 baz)").
/// * Note: the cheatcode will output the canonical type even if the input is malformated
/// with the wrong order of elements or with extra whitespaces.
#[cheatcode(group = Utilities)]
function eip712HashType(string calldata typeNameOrDefinition) external pure returns (bytes32 typeHash);

/// Generates the hash of the canonical EIP-712 type representation.
/// Requires previous binding generation with `forge bind-json`.
///
/// Params:
/// * `bindingsPath`: path where the output of `forge bind-json` is stored.
/// * `typeName`: Name of the type (i.e. "Transaction").
#[cheatcode(group = Utilities)]
function eip712HashType(string calldata bindingsPath, string calldata typeName) external pure returns (bytes32 typeHash);

/// Generates the struct hash of the canonical EIP-712 type representation and its abi-encoded data.
///
/// Supports 2 different inputs:
/// 1. Name of the type (i.e. "PermitSingle"):
/// * requires previous binding generation with `forge bind-json`.
/// * bindings will be retrieved from the path configured in `foundry.toml`.
///
/// 2. String representation of the type (i.e. "Foo(Bar bar) Bar(uint256 baz)").
/// * Note: the cheatcode will use the canonical type even if the input is malformated
/// with the wrong order of elements or with extra whitespaces.
#[cheatcode(group = Utilities)]
function eip712HashStruct(string calldata typeNameOrDefinition, bytes calldata abiEncodedData) external pure returns (bytes32 typeHash);

/// Generates the struct hash of the canonical EIP-712 type representation and its abi-encoded data.
/// Requires previous binding generation with `forge bind-json`.
///
/// Params:
/// * `bindingsPath`: path where the output of `forge bind-json` is stored.
/// * `typeName`: Name of the type (i.e. "PermitSingle").
/// * `abiEncodedData`: ABI-encoded data for the struct that is being hashed.
#[cheatcode(group = Utilities)]
function eip712HashStruct(string calldata bindingsPath, string calldata typeName, bytes calldata abiEncodedData) external pure returns (bytes32 typeHash);

/// Generates a ready-to-sign digest of human-readable typed data following the EIP-712 standard.
#[cheatcode(group = Utilities)]
function eip712HashTypedData(string calldata jsonData) external pure returns (bytes32 digest);
}
}

Expand Down
4 changes: 4 additions & 0 deletions crates/cheatcodes/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ pub struct CheatsConfig {
pub rpc_endpoints: ResolvedRpcEndpoints,
/// Project's paths as configured
pub paths: ProjectPathsConfig,
/// Path to the directory that contains the bindings generated by `forge bind-json`.
pub bind_json_path: PathBuf,
/// Filesystem permissions for cheatcodes like `writeFile`, `readFile`
pub fs_permissions: FsPermissions,
/// Project root
Expand Down Expand Up @@ -98,6 +100,7 @@ impl CheatsConfig {
no_storage_caching: config.no_storage_caching,
rpc_endpoints,
paths: config.project_paths(),
bind_json_path: config.bind_json.out.clone(),
fs_permissions: config.fs_permissions.clone().joined(config.root.as_ref()),
root: config.root.clone(),
broadcast: config.root.clone().join(&config.broadcast),
Expand Down Expand Up @@ -303,6 +306,7 @@ impl Default for CheatsConfig {
paths: ProjectPathsConfig::builder().build_with_root("./"),
fs_permissions: Default::default(),
root: Default::default(),
bind_json_path: PathBuf::default().join("utils").join("jsonBindings.sol"),
broadcast: Default::default(),
allowed_paths: vec![],
evm_opts: Default::default(),
Expand Down
152 changes: 150 additions & 2 deletions crates/cheatcodes/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
//! Implementations of [`Utilities`](spec::Group::Utilities) cheatcodes.

use crate::{Cheatcode, Cheatcodes, CheatcodesExecutor, CheatsCtxt, Result, Vm::*};
use alloy_dyn_abi::{DynSolType, DynSolValue};
use alloy_dyn_abi::{eip712_parser::EncodeType, DynSolType, DynSolValue, Resolver, TypedData};
use alloy_ens::namehash;
use alloy_primitives::{aliases::B32, map::HashMap, B64, U256};
use alloy_primitives::{aliases::B32, keccak256, map::HashMap, Bytes, B64, U256};
use alloy_sol_types::SolValue;
use foundry_common::{fs, TYPE_BINDING_PREFIX};
use foundry_config::fs_permissions::FsAccessKind;
use foundry_evm_core::constants::DEFAULT_CREATE2_DEPLOYER;
use proptest::prelude::Strategy;
use rand::{seq::SliceRandom, Rng, RngCore};
use revm::context::JournalTr;
use std::path::PathBuf;

/// Contains locations of traces ignored via cheatcodes.
///
Expand Down Expand Up @@ -314,3 +317,148 @@ fn random_int(state: &mut Cheatcodes, bits: Option<U256>) -> Result {
.current()
.abi_encode())
}

impl Cheatcode for eip712HashType_0Call {
fn apply(&self, state: &mut Cheatcodes) -> Result {
let Self { typeNameOrDefinition } = self;

let type_def = get_canonical_type_def(typeNameOrDefinition, state, None)?;

Ok(keccak256(type_def.as_bytes()).to_vec())
}
}

impl Cheatcode for eip712HashType_1Call {
fn apply(&self, state: &mut Cheatcodes) -> Result {
let Self { bindingsPath, typeName } = self;

let path = state.config.ensure_path_allowed(bindingsPath, FsAccessKind::Read)?;
let type_def = get_type_def_from_bindings(typeName, path, &state.config.root)?;

Ok(keccak256(type_def.as_bytes()).to_vec())
}
}

impl Cheatcode for eip712HashStruct_0Call {
fn apply(&self, state: &mut Cheatcodes) -> Result {
let Self { typeNameOrDefinition, abiEncodedData } = self;

let type_def = get_canonical_type_def(typeNameOrDefinition, state, None)?;
let primary = &type_def[..type_def.find('(').unwrap_or(type_def.len())];

get_struct_hash(primary, &type_def, abiEncodedData)
}
}

impl Cheatcode for eip712HashStruct_1Call {
fn apply(&self, state: &mut Cheatcodes) -> Result {
let Self { bindingsPath, typeName, abiEncodedData } = self;

let path = state.config.ensure_path_allowed(bindingsPath, FsAccessKind::Read)?;
let type_def = get_type_def_from_bindings(typeName, path, &state.config.root)?;

get_struct_hash(typeName, &type_def, abiEncodedData)
}
}

impl Cheatcode for eip712HashTypedDataCall {
fn apply(&self, _state: &mut Cheatcodes) -> Result {
let Self { jsonData } = self;
let typed_data: TypedData = serde_json::from_str(jsonData)?;
let digest = typed_data.eip712_signing_hash()?;

Ok(digest.to_vec())
}
}

/// Returns EIP-712 canonical type definition from the provided string type representation or type
/// name. If type name provided, then it looks up bindings from file generated by `forge bind-json`.
fn get_canonical_type_def(
name_or_def: &String,
state: &mut Cheatcodes,
path: Option<PathBuf>,
) -> Result<String> {
let type_def = if name_or_def.contains('(') {
// If the input contains '(', it must be the type definition.
EncodeType::parse(name_or_def).and_then(|parsed| parsed.canonicalize())?
} else {
// Otherwise, it must be the type name.
let path = path.as_ref().unwrap_or(&state.config.bind_json_path);
let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
get_type_def_from_bindings(name_or_def, path, &state.config.root)?
};

Ok(type_def)
}

/// Returns the EIP-712 type definition from the bindings in the provided path.
/// Assumes that read validation for the path has already been checked.
fn get_type_def_from_bindings(name: &String, path: PathBuf, root: &PathBuf) -> Result<String> {
let content = fs::read_to_string(&path)?;

let type_defs: HashMap<&str, &str> = content
.lines()
.filter_map(|line| {
let relevant = line.trim().strip_prefix(TYPE_BINDING_PREFIX)?;
let (name, def) = relevant.split_once('=')?;
Some((name.trim(), def.trim().strip_prefix('"')?.strip_suffix("\";")?))
})
.collect();

match type_defs.get(name.as_str()) {
Some(value) => Ok(value.to_string()),
None => {
let bindings =
type_defs.keys().map(|k| format!(" - {k}")).collect::<Vec<String>>().join("\n");

bail!(
"'{}' not found in '{}'.{}",
name,
path.strip_prefix(root).unwrap_or(&path).to_string_lossy(),
if bindings.is_empty() {
String::new()
} else {
format!("\nAvailable bindings:\n{bindings}\n")
}
);
}
}
}

/// Returns the EIP-712 struct hash for provided name, definition and ABI encoded data.
fn get_struct_hash(primary: &str, type_def: &String, abi_encoded_data: &Bytes) -> Result {
let mut resolver = Resolver::default();

// Populate the resolver by ingesting the canonical type definition, and then get the
// corresponding `DynSolType` of the primary type.
resolver
.ingest_string(type_def)
.map_err(|e| fmt_err!("Resolver failed to ingest type definition: {e}"))?;

let resolved_sol_type = resolver
.resolve(primary)
.map_err(|e| fmt_err!("Failed to resolve EIP-712 primary type '{primary}': {e}"))?;

// ABI-decode the bytes into `DynSolValue::CustomStruct`.
let sol_value = resolved_sol_type.abi_decode(abi_encoded_data.as_ref()).map_err(|e| {
fmt_err!("Failed to ABI decode using resolved_sol_type directly for '{primary}': {e}.")
})?;

// Use the resolver to properly encode the data.
let encoded_data: Vec<u8> = resolver
.encode_data(&sol_value)
.map_err(|e| fmt_err!("Failed to EIP-712 encode data for struct '{primary}': {e}"))?
.ok_or_else(|| fmt_err!("EIP-712 data encoding returned 'None' for struct '{primary}'"))?;

// Compute the type hash of the primary type.
let type_hash = resolver
.type_hash(primary)
.map_err(|e| fmt_err!("Failed to compute typeHash for EIP712 type '{primary}': {e}"))?;

// Compute the struct hash of the concatenated type hash and encoded data.
let mut bytes_to_hash = Vec::with_capacity(32 + encoded_data.len());
bytes_to_hash.extend_from_slice(type_hash.as_slice());
bytes_to_hash.extend_from_slice(&encoded_data);

Ok(keccak256(&bytes_to_hash).to_vec())
}
Loading
Loading