Skip to content

chore(forge): cheat eip712 struct hash #10626

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 10 commits into from
May 29, 2025
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
40 changes: 40 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.

23 changes: 23 additions & 0 deletions crates/cheatcodes/spec/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2910,6 +2910,29 @@ interface Vm {
/// * `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);
}
}

Expand Down
97 changes: 83 additions & 14 deletions crates/cheatcodes/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
//! Implementations of [`Utilities`](spec::Group::Utilities) cheatcodes.

use crate::{Cheatcode, Cheatcodes, CheatcodesExecutor, CheatsCtxt, Result, Vm::*};
use alloy_dyn_abi::{eip712_parser::EncodeType, DynSolType, DynSolValue};
use alloy_primitives::{aliases::B32, keccak256, map::HashMap, B64, U256};
use alloy_dyn_abi::{eip712_parser::EncodeType, DynSolType, DynSolValue, Resolver};
use alloy_primitives::{aliases::B32, keccak256, map::HashMap, Bytes, B64, U256};
use alloy_sol_types::SolValue;
use foundry_common::{ens::namehash, fs, TYPE_BINDING_PREFIX};
use foundry_config::fs_permissions::FsAccessKind;
Expand Down Expand Up @@ -321,16 +321,7 @@ impl Cheatcode for eip712HashType_0Call {
fn apply(&self, state: &mut Cheatcodes) -> Result {
let Self { typeNameOrDefinition } = self;

let type_def = if typeNameOrDefinition.contains('(') {
// If the input contains '(', it must be the type definition
EncodeType::parse(typeNameOrDefinition).and_then(|parsed| parsed.canonicalize())?
} else {
// Otherwise, it must be the type name
let path = state
.config
.ensure_path_allowed(&state.config.bind_json_path, FsAccessKind::Read)?;
get_type_def_from_bindings(typeNameOrDefinition, path, &state.config.root)?
};
let type_def = get_canonical_type_def(typeNameOrDefinition, state, None)?;

Ok(keccak256(type_def.as_bytes()).to_vec())
}
Expand All @@ -347,8 +338,49 @@ impl Cheatcode for eip712HashType_1Call {
}
}

/// Gets the type definition from the bindings in the provided path. Assumes that read validation
/// for the path has already been checked.
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)
}
}

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)
}

/// Gets the 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)?;

Expand Down Expand Up @@ -380,3 +412,40 @@ fn get_type_def_from_bindings(name: &String, path: PathBuf, root: &PathBuf) -> R
}
}
}

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