Skip to content

Commit 4b23502

Browse files
authored
chore(forge): cheat eip712 struct hash (#10626)
1 parent c6f037d commit 4b23502

File tree

5 files changed

+451
-14
lines changed

5 files changed

+451
-14
lines changed

crates/cheatcodes/assets/cheatcodes.json

Lines changed: 40 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: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2910,6 +2910,29 @@ interface Vm {
29102910
/// * `typeName`: Name of the type (i.e. "Transaction").
29112911
#[cheatcode(group = Utilities)]
29122912
function eip712HashType(string calldata bindingsPath, string calldata typeName) external pure returns (bytes32 typeHash);
2913+
2914+
/// Generates the struct hash of the canonical EIP-712 type representation and its abi-encoded data.
2915+
///
2916+
/// Supports 2 different inputs:
2917+
/// 1. Name of the type (i.e. "PermitSingle"):
2918+
/// * requires previous binding generation with `forge bind-json`.
2919+
/// * bindings will be retrieved from the path configured in `foundry.toml`.
2920+
///
2921+
/// 2. String representation of the type (i.e. "Foo(Bar bar) Bar(uint256 baz)").
2922+
/// * Note: the cheatcode will use the canonical type even if the input is malformated
2923+
/// with the wrong order of elements or with extra whitespaces.
2924+
#[cheatcode(group = Utilities)]
2925+
function eip712HashStruct(string calldata typeNameOrDefinition, bytes calldata abiEncodedData) external pure returns (bytes32 typeHash);
2926+
2927+
/// Generates the struct hash of the canonical EIP-712 type representation and its abi-encoded data.
2928+
/// Requires previous binding generation with `forge bind-json`.
2929+
///
2930+
/// Params:
2931+
/// * `bindingsPath`: path where the output of `forge bind-json` is stored.
2932+
/// * `typeName`: Name of the type (i.e. "PermitSingle").
2933+
/// * `abiEncodedData`: ABI-encoded data for the struct that is being hashed.
2934+
#[cheatcode(group = Utilities)]
2935+
function eip712HashStruct(string calldata bindingsPath, string calldata typeName, bytes calldata abiEncodedData) external pure returns (bytes32 typeHash);
29132936
}
29142937
}
29152938

crates/cheatcodes/src/utils.rs

Lines changed: 83 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
//! Implementations of [`Utilities`](spec::Group::Utilities) cheatcodes.
22
33
use crate::{Cheatcode, Cheatcodes, CheatcodesExecutor, CheatsCtxt, Result, Vm::*};
4-
use alloy_dyn_abi::{eip712_parser::EncodeType, DynSolType, DynSolValue};
5-
use alloy_primitives::{aliases::B32, keccak256, map::HashMap, B64, U256};
4+
use alloy_dyn_abi::{eip712_parser::EncodeType, DynSolType, DynSolValue, Resolver};
5+
use alloy_primitives::{aliases::B32, keccak256, map::HashMap, Bytes, B64, U256};
66
use alloy_sol_types::SolValue;
77
use foundry_common::{ens::namehash, fs, TYPE_BINDING_PREFIX};
88
use foundry_config::fs_permissions::FsAccessKind;
@@ -321,16 +321,7 @@ impl Cheatcode for eip712HashType_0Call {
321321
fn apply(&self, state: &mut Cheatcodes) -> Result {
322322
let Self { typeNameOrDefinition } = self;
323323

324-
let type_def = if typeNameOrDefinition.contains('(') {
325-
// If the input contains '(', it must be the type definition
326-
EncodeType::parse(typeNameOrDefinition).and_then(|parsed| parsed.canonicalize())?
327-
} else {
328-
// Otherwise, it must be the type name
329-
let path = state
330-
.config
331-
.ensure_path_allowed(&state.config.bind_json_path, FsAccessKind::Read)?;
332-
get_type_def_from_bindings(typeNameOrDefinition, path, &state.config.root)?
333-
};
324+
let type_def = get_canonical_type_def(typeNameOrDefinition, state, None)?;
334325

335326
Ok(keccak256(type_def.as_bytes()).to_vec())
336327
}
@@ -347,8 +338,49 @@ impl Cheatcode for eip712HashType_1Call {
347338
}
348339
}
349340

350-
/// Gets the type definition from the bindings in the provided path. Assumes that read validation
351-
/// for the path has already been checked.
341+
impl Cheatcode for eip712HashStruct_0Call {
342+
fn apply(&self, state: &mut Cheatcodes) -> Result {
343+
let Self { typeNameOrDefinition, abiEncodedData } = self;
344+
345+
let type_def = get_canonical_type_def(typeNameOrDefinition, state, None)?;
346+
let primary = &type_def[..type_def.find('(').unwrap_or(type_def.len())];
347+
348+
get_struct_hash(primary, &type_def, abiEncodedData)
349+
}
350+
}
351+
352+
impl Cheatcode for eip712HashStruct_1Call {
353+
fn apply(&self, state: &mut Cheatcodes) -> Result {
354+
let Self { bindingsPath, typeName, abiEncodedData } = self;
355+
356+
let path = state.config.ensure_path_allowed(bindingsPath, FsAccessKind::Read)?;
357+
let type_def = get_type_def_from_bindings(typeName, path, &state.config.root)?;
358+
359+
get_struct_hash(typeName, &type_def, abiEncodedData)
360+
}
361+
}
362+
363+
fn get_canonical_type_def(
364+
name_or_def: &String,
365+
state: &mut Cheatcodes,
366+
path: Option<PathBuf>,
367+
) -> Result<String> {
368+
let type_def = if name_or_def.contains('(') {
369+
// If the input contains '(', it must be the type definition
370+
EncodeType::parse(name_or_def).and_then(|parsed| parsed.canonicalize())?
371+
} else {
372+
// Otherwise, it must be the type name
373+
let path = path.as_ref().unwrap_or(&state.config.bind_json_path);
374+
let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?;
375+
get_type_def_from_bindings(name_or_def, path, &state.config.root)?
376+
};
377+
378+
Ok(type_def)
379+
}
380+
381+
/// Gets the type definition from the bindings in the provided path.
382+
///
383+
/// Assumes that read validation for the path has already been checked.
352384
fn get_type_def_from_bindings(name: &String, path: PathBuf, root: &PathBuf) -> Result<String> {
353385
let content = fs::read_to_string(&path)?;
354386

@@ -380,3 +412,40 @@ fn get_type_def_from_bindings(name: &String, path: PathBuf, root: &PathBuf) -> R
380412
}
381413
}
382414
}
415+
416+
fn get_struct_hash(primary: &str, type_def: &String, abi_encoded_data: &Bytes) -> Result {
417+
let mut resolver = Resolver::default();
418+
419+
// Populate the resolver by ingesting the canonical type definition, and then get the
420+
// corresponding `DynSolType` of the primary type.
421+
resolver
422+
.ingest_string(type_def)
423+
.map_err(|e| fmt_err!("Resolver failed to ingest type definition: {e}"))?;
424+
425+
let resolved_sol_type = resolver
426+
.resolve(primary)
427+
.map_err(|e| fmt_err!("Failed to resolve EIP-712 primary type '{primary}': {e}"))?;
428+
429+
// ABI-decode the bytes into `DynSolValue::CustomStruct`.
430+
let sol_value = resolved_sol_type.abi_decode(abi_encoded_data.as_ref()).map_err(|e| {
431+
fmt_err!("Failed to ABI decode using resolved_sol_type directly for '{primary}': {e}.")
432+
})?;
433+
434+
// Use the resolver to properly encode the data.
435+
let encoded_data: Vec<u8> = resolver
436+
.encode_data(&sol_value)
437+
.map_err(|e| fmt_err!("Failed to EIP-712 encode data for struct '{primary}': {e}"))?
438+
.ok_or_else(|| fmt_err!("EIP-712 data encoding returned 'None' for struct '{primary}'"))?;
439+
440+
// Compute the type hash of the primary type.
441+
let type_hash = resolver
442+
.type_hash(primary)
443+
.map_err(|e| fmt_err!("Failed to compute typeHash for EIP712 type '{primary}': {e}"))?;
444+
445+
// Compute the struct hash of the concatenated type hash and encoded data.
446+
let mut bytes_to_hash = Vec::with_capacity(32 + encoded_data.len());
447+
bytes_to_hash.extend_from_slice(type_hash.as_slice());
448+
bytes_to_hash.extend_from_slice(&encoded_data);
449+
450+
Ok(keccak256(&bytes_to_hash).to_vec())
451+
}

0 commit comments

Comments
 (0)