From d266534e0e4a3a15ccd846fb8b94f950a098a5f8 Mon Sep 17 00:00:00 2001
From: 0xrusowsky <0xrusowsky@proton.me>
Date: Sat, 24 May 2025 22:12:40 -0500
Subject: [PATCH 01/10] eip712 struct hash cheatcodes
---
crates/cheatcodes/assets/cheatcodes.json | 40 ++++++++
crates/cheatcodes/spec/src/vm.rs | 6 ++
crates/cheatcodes/src/json.rs | 16 +++-
crates/cheatcodes/src/utils.rs | 104 ++++++++++++++++++---
crates/forge/tests/cli/test_cmd.rs | 111 +++++++++++++++++++++++
testdata/cheats/Vm.sol | 2 +
6 files changed, 262 insertions(+), 17 deletions(-)
diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json
index e00c1f2125891..a3a8ee234b922 100644
--- a/crates/cheatcodes/assets/cheatcodes.json
+++ b/crates/cheatcodes/assets/cheatcodes.json
@@ -4294,6 +4294,46 @@
"status": "stable",
"safety": "unsafe"
},
+ {
+ "func": {
+ "id": "eip712HashStruct_0",
+ "description": "",
+ "declaration": "function eip712HashStruct(string calldata typeNameOrDefinition, string calldata jsonData) external pure returns (bytes32 typeHash);",
+ "visibility": "external",
+ "mutability": "pure",
+ "signature": "eip712HashStruct(string,string)",
+ "selector": "0x686230fc",
+ "selectorBytes": [
+ 104,
+ 98,
+ 48,
+ 252
+ ]
+ },
+ "group": "utilities",
+ "status": "stable",
+ "safety": "safe"
+ },
+ {
+ "func": {
+ "id": "eip712HashStruct_1",
+ "description": "",
+ "declaration": "function eip712HashStruct(string calldata bindingsPath, string calldata typeName, string calldata jsonData) external pure returns (bytes32 typeHash);",
+ "visibility": "external",
+ "mutability": "pure",
+ "signature": "eip712HashStruct(string,string,string)",
+ "selector": "0x0e7d8d44",
+ "selectorBytes": [
+ 14,
+ 125,
+ 141,
+ 68
+ ]
+ },
+ "group": "utilities",
+ "status": "stable",
+ "safety": "safe"
+ },
{
"func": {
"id": "eip712HashType_0",
diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs
index f9ed82ee4550e..11be740237c69 100644
--- a/crates/cheatcodes/spec/src/vm.rs
+++ b/crates/cheatcodes/spec/src/vm.rs
@@ -2910,6 +2910,12 @@ 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);
+
+ #[cheatcode(group = Utilities)]
+ function eip712HashStruct(string calldata typeNameOrDefinition, string calldata jsonData) external pure returns (bytes32 typeHash);
+
+ #[cheatcode(group = Utilities)]
+ function eip712HashStruct(string calldata bindingsPath, string calldata typeName, string calldata jsonData) external pure returns (bytes32 typeHash);
}
}
diff --git a/crates/cheatcodes/src/json.rs b/crates/cheatcodes/src/json.rs
index 6ad36e4742901..e6af79469f0b5 100644
--- a/crates/cheatcodes/src/json.rs
+++ b/crates/cheatcodes/src/json.rs
@@ -2,7 +2,7 @@
use crate::{string, Cheatcode, Cheatcodes, Result, Vm::*};
use alloy_dyn_abi::{eip712_parser::EncodeType, DynSolType, DynSolValue, Resolver};
-use alloy_primitives::{hex, Address, B256, I256};
+use alloy_primitives::{hex, Address, B256, I256, U256};
use alloy_sol_types::SolValue;
use foundry_common::fs;
use foundry_config::fs_permissions::FsAccessKind;
@@ -605,14 +605,22 @@ fn serialize_value_as_json(value: DynSolValue) -> Result {
DynSolValue::Bytes(b) => Ok(Value::String(hex::encode_prefixed(b))),
DynSolValue::FixedBytes(b, size) => Ok(Value::String(hex::encode_prefixed(&b[..size]))),
DynSolValue::Int(i, _) => {
+ // TODO: same problem as with Uint
// let serde handle number parsing
let n = serde_json::from_str(&i.to_string())?;
Ok(Value::Number(n))
}
DynSolValue::Uint(i, _) => {
- // let serde handle number parsing
- let n = serde_json::from_str(&i.to_string())?;
- Ok(Value::Number(n))
+ // TODO: ask Arsenii if (or always using strings) this is acceptable
+ // needed cause otherwise alloy fails to coerce numbers > u64
+ // alternatively i could do an alloy PR
+ if i <= U256::from(u64::MAX) {
+ // let serde handle number parsing
+ let n = serde_json::from_str(&i.to_string())?;
+ Ok(Value::Number(n))
+ } else {
+ Ok(Value::String(i.to_string()))
+ }
}
DynSolValue::Address(a) => Ok(Value::String(a.to_string())),
DynSolValue::Array(e) | DynSolValue::FixedArray(e) => {
diff --git a/crates/cheatcodes/src/utils.rs b/crates/cheatcodes/src/utils.rs
index 8f0ebb44f6293..6cf562da87a0a 100644
--- a/crates/cheatcodes/src/utils.rs
+++ b/crates/cheatcodes/src/utils.rs
@@ -1,7 +1,7 @@
//! 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_dyn_abi::{eip712_parser::EncodeType, DynSolType, DynSolValue, Resolver};
use alloy_primitives::{aliases::B32, keccak256, map::HashMap, B64, U256};
use alloy_sol_types::SolValue;
use foundry_common::{ens::namehash, fs, TYPE_BINDING_PREFIX};
@@ -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())
}
@@ -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, jsonData } = self;
+
+ let type_def = get_canonical_type_def(typeNameOrDefinition, state, None)?;
+ let primary = &type_def[..type_def.find('(').unwrap_or_else(|| type_def.len())];
+
+ get_struct_hash(primary, &type_def, jsonData)
+ }
+}
+
+impl Cheatcode for eip712HashStruct_1Call {
+ fn apply(&self, state: &mut Cheatcodes) -> Result {
+ let Self { bindingsPath, typeName, jsonData } = 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, jsonData)
+ }
+}
+
+fn get_canonical_type_def(
+ name_or_def: &String,
+ state: &mut Cheatcodes,
+ path: Option,
+) -> Result {
+ 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 {
let content = fs::read_to_string(&path)?;
@@ -380,3 +412,49 @@ fn get_type_def_from_bindings(name: &String, path: PathBuf, root: &PathBuf) -> R
}
}
}
+
+fn get_struct_hash(primary: &str, type_def: &String, json_data: &String) -> 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 EIP712 primary type '{}' using resolver: {}", primary, e)
+ })?;
+
+ // Coerce the JSON data into a `DynSolValue`.
+ let json_data = serde_json::from_str(json_data)
+ .map_err(|e| fmt_err!("Failed to parse input JSON data: {}", e))?;
+
+ let sol_value = resolved_sol_type.coerce_json(&json_data).map_err(|e| {
+ fmt_err!("Failed to coerce JSON data to EIP-712 'SolValue' for type '{}': {}", primary, e)
+ })?;
+
+ if !matches!(sol_value, DynSolValue::CustomStruct { .. }) {
+ bail!("JSON data for type '{}' is not a custom Struct", primary);
+ }
+
+ // Use the resolver to properly encode the data.
+ let encoded_data: Vec = 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())
+}
diff --git a/crates/forge/tests/cli/test_cmd.rs b/crates/forge/tests/cli/test_cmd.rs
index fba03905afd8c..440f19d7cdc45 100644
--- a/crates/forge/tests/cli/test_cmd.rs
+++ b/crates/forge/tests/cli/test_cmd.rs
@@ -3883,3 +3883,114 @@ Encountered a total of 1 failing tests, 0 tests succeeded
])
.assert_success();
});
+
+forgetest!(test_eip712_hash_struct, |prj, cmd| {
+ prj.add_source(
+ "Eip712Permit.sol",
+ r#"
+struct PermitDetails {
+ address token;
+ uint160 amount;
+ uint48 expiration;
+ uint48 nonce;
+}
+
+// Canonical type hash for PermitDetails
+bytes32 constant _PERMIT_DETAILS_TYPEHASH = keccak256(
+ "PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)"
+);
+
+struct PermitSingle {
+ PermitDetails details;
+ address spender;
+ uint256 sigDeadline;
+}
+
+// Canonical type hash for PermitSingle
+bytes32 constant _PERMIT_SINGLE_TYPEHASH = keccak256(
+ "PermitSingle(PermitDetails details,address spender,uint256 sigDeadline)PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)"
+);
+
+// borrowed from https://github.com/Uniswap/permit2/blob/main/src/libraries/PermitHash.sol
+library PermitHash {
+ function hash(PermitSingle memory permitSingle) internal pure returns (bytes32) {
+ bytes32 permitHash = _hashPermitDetails(permitSingle.details);
+ return
+ keccak256(abi.encode(_PERMIT_SINGLE_TYPEHASH, permitHash, permitSingle.spender, permitSingle.sigDeadline));
+ }
+
+ function _hashPermitDetails(PermitDetails memory details) private pure returns (bytes32) {
+ return keccak256(abi.encode(_PERMIT_DETAILS_TYPEHASH, details));
+ }
+}
+"#,
+ )
+ .unwrap();
+
+ let bindings = prj.root().join("utils").join("JsonBindings.sol");
+ prj.update_config(|config| config.fs_permissions.add(PathPermission::read(&bindings)));
+ cmd.forge_fuse().args(["bind-json"]).assert_success();
+
+ prj.insert_ds_test();
+ prj.insert_vm();
+ prj.insert_console();
+ prj.add_source(
+ "Eip712HashStructTest.sol",
+ r#"
+import "./Vm.sol";
+import "./test.sol";
+import "./console.sol";
+import "./Eip712Permit.sol";
+import {JsonBindings} from "utils/JsonBindings.sol";
+
+
+contract Eip712HashStructTest is DSTest {
+ Vm constant vm = Vm(HEVM_ADDRESS);
+ using JsonBindings for *;
+
+ function testHashPermitSingle() public {
+ PermitDetails memory details = PermitDetails({
+ token: 0x1111111111111111111111111111111111111111,
+ amount: 1000 ether,
+ expiration: 12345,
+ nonce: 1
+ });
+
+ PermitSingle memory permit = PermitSingle({
+ details: details,
+ spender: 0x2222222222222222222222222222222222222222,
+ sigDeadline: 12345
+ });
+
+ // user-computed permit (using uniswap hash library)
+ bytes32 userStructHash = PermitHash.hash(permit);
+
+ // cheatcode-computed permit (previously serializing to JSON)
+ string memory jsonData = permit.serialize();
+
+ console.log("JSON data for PermitSingle:");
+ console.log(jsonData);
+
+ bytes32 cheatStructHash = vm.eip712HashStruct("PermitSingle", jsonData);
+ console.log("PermitSingle struct hash from cheatcode:");
+ console.logBytes32(cheatStructHash);
+
+ assertEq(cheatStructHash, userStructHash, "permit struct hash mismatch");
+ }
+}
+"#,
+ )
+ .unwrap();
+
+ cmd.forge_fuse()
+ .args(["test", "--mc", "Eip712HashStructTest", "-vv"])
+ .assert_success()
+ .stdout_eq(str![[r#"
+...
+ JSON data for PermitSingle:
+ {"details":{"token":"0x1111111111111111111111111111111111111111","amount":"1000000000000000000000","expiration":12345,"nonce":1},"spender":"0x2222222222222222222222222222222222222222","sigDeadline":12345}
+ PermitSingle struct hash from cheatcode:
+ 0x3ed744fdcea02b6b9ad45a9db6e648bf6f18c221909f9ee425191f2a02f9e4a8
+...
+"#]]);
+});
diff --git a/testdata/cheats/Vm.sol b/testdata/cheats/Vm.sol
index ac5928158b647..d7396d7b83916 100644
--- a/testdata/cheats/Vm.sol
+++ b/testdata/cheats/Vm.sol
@@ -208,6 +208,8 @@ interface Vm {
function deriveKey(string calldata mnemonic, string calldata derivationPath, uint32 index, string calldata language) external pure returns (uint256 privateKey);
function difficulty(uint256 newDifficulty) external;
function dumpState(string calldata pathToStateJson) external;
+ function eip712HashStruct(string calldata typeNameOrDefinition, string calldata jsonData) external pure returns (bytes32 typeHash);
+ function eip712HashStruct(string calldata bindingsPath, string calldata typeName, string calldata jsonData) external pure returns (bytes32 typeHash);
function eip712HashType(string calldata typeNameOrDefinition) external pure returns (bytes32 typeHash);
function eip712HashType(string calldata bindingsPath, string calldata typeName) external pure returns (bytes32 typeHash);
function ensNamehash(string calldata name) external pure returns (bytes32);
From 53fac1f16c0c967c3984538890a5c59e5bb7cc30 Mon Sep 17 00:00:00 2001
From: 0xrusowsky <0xrusowsky@proton.me>
Date: Sat, 24 May 2025 22:39:32 -0500
Subject: [PATCH 02/10] docs: comments
---
crates/cheatcodes/src/json.rs | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/crates/cheatcodes/src/json.rs b/crates/cheatcodes/src/json.rs
index e6af79469f0b5..78c1febfdc89c 100644
--- a/crates/cheatcodes/src/json.rs
+++ b/crates/cheatcodes/src/json.rs
@@ -611,9 +611,9 @@ fn serialize_value_as_json(value: DynSolValue) -> Result {
Ok(Value::Number(n))
}
DynSolValue::Uint(i, _) => {
- // TODO: ask Arsenii if (or always using strings) this is acceptable
- // needed cause otherwise alloy fails to coerce numbers > u64
- // alternatively i could do an alloy PR
+ // TODO: ask Arsenii if this approach (or always using strings) is acceptable.
+ // needed cause otherwise alloy fails to coerce numbers > u64.
+ // alternatively i could do an alloy PR.
if i <= U256::from(u64::MAX) {
// let serde handle number parsing
let n = serde_json::from_str(&i.to_string())?;
From 9f6c0fede1b324e982ed154bb9300cc42e342a87 Mon Sep 17 00:00:00 2001
From: 0xrusowsky <0xrusowsky@proton.me>
Date: Sun, 25 May 2025 21:39:37 -0500
Subject: [PATCH 03/10] clippy + no bindings test
---
crates/cheatcodes/src/utils.rs | 10 ++---
crates/forge/tests/cli/test_cmd.rs | 66 +++++++++++++++++++++++++++++-
2 files changed, 70 insertions(+), 6 deletions(-)
diff --git a/crates/cheatcodes/src/utils.rs b/crates/cheatcodes/src/utils.rs
index 6cf562da87a0a..295d02b8fe224 100644
--- a/crates/cheatcodes/src/utils.rs
+++ b/crates/cheatcodes/src/utils.rs
@@ -343,7 +343,7 @@ impl Cheatcode for eip712HashStruct_0Call {
let Self { typeNameOrDefinition, jsonData } = self;
let type_def = get_canonical_type_def(typeNameOrDefinition, state, None)?;
- let primary = &type_def[..type_def.find('(').unwrap_or_else(|| type_def.len())];
+ let primary = &type_def[..type_def.find('(').unwrap_or(type_def.len())];
get_struct_hash(primary, &type_def, jsonData)
}
@@ -413,16 +413,16 @@ fn get_type_def_from_bindings(name: &String, path: PathBuf, root: &PathBuf) -> R
}
}
-fn get_struct_hash(primary: &str, type_def: &String, json_data: &String) -> Result {
+fn get_struct_hash(primary: &str, type_def: &String, json_data: &str) -> 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)
+ .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| {
+ let resolved_sol_type = resolver.resolve(primary).map_err(|e| {
fmt_err!("Failed to resolve EIP712 primary type '{}' using resolver: {}", primary, e)
})?;
@@ -448,7 +448,7 @@ fn get_struct_hash(primary: &str, type_def: &String, json_data: &String) -> Resu
// Compute the type hash of the primary type.
let type_hash = resolver
- .type_hash(&primary)
+ .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.
diff --git a/crates/forge/tests/cli/test_cmd.rs b/crates/forge/tests/cli/test_cmd.rs
index 440f19d7cdc45..d49bb6d9aab8c 100644
--- a/crates/forge/tests/cli/test_cmd.rs
+++ b/crates/forge/tests/cli/test_cmd.rs
@@ -3884,7 +3884,71 @@ Encountered a total of 1 failing tests, 0 tests succeeded
.assert_success();
});
-forgetest!(test_eip712_hash_struct, |prj, cmd| {
+forgetest!(test_eip712_hash_struct_simple_domain, |prj, cmd| {
+ prj.insert_ds_test();
+ prj.insert_vm();
+ prj.insert_console();
+
+ prj.add_source(
+ "Eip712HashStructDomainTest.sol",
+ r#"
+import "./Vm.sol";
+import "./test.sol";
+import "./console.sol";
+
+struct EIP712Domain {
+ string name;
+ string version;
+ uint256 chainId;
+ address verifyingContract;
+}
+
+string constant _EIP712_DOMAIN_TYPE_DEF = "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)";
+bytes32 constant _EIP712_DOMAIN_TYPE_HASH = keccak256(bytes(_EIP712_DOMAIN_TYPE_DEF));
+
+contract Eip712HashStructDomainTest is DSTest {
+ Vm constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code")))));
+
+ function testHashEIP712Domain() public {
+ EIP712Domain memory domain = EIP712Domain({
+ name: "Foo",
+ version: "Bar",
+ chainId: 1,
+ verifyingContract: 0xdEADBEeF00000000000000000000000000000000
+ });
+
+ // simulate user-computed hash
+ bytes memory encodedData = abi.encode(
+ keccak256(bytes(domain.name)),
+ keccak256(bytes(domain.version)),
+ bytes32(domain.chainId),
+ bytes32(uint256(uint160(domain.verifyingContract)))
+ );
+ bytes32 userStructHash = keccak256(abi.encodePacked(_EIP712_DOMAIN_TYPE_HASH, encodedData));
+
+ // cheatcode-computed permit (manually serializing to JSON)
+ string memory jsonData = vm.serializeString("domain", "name", domain.name);
+ jsonData = vm.serializeString("domain", "version", domain.version);
+ jsonData = vm.serializeUint("domain", "chainId", domain.chainId);
+ jsonData = vm.serializeAddress("domain", "verifyingContract", domain.verifyingContract);
+ console.log("JSON Data for EIP712Domain:");
+ console.log(jsonData);
+
+ bytes32 cheatStructHash = vm.eip712HashStruct(_EIP712_DOMAIN_TYPE_DEF, jsonData);
+ console.log("EIP712Domain struct hash from cheatcode:");
+ console.logBytes32(cheatStructHash);
+
+ assertEq(cheatStructHash, userStructHash, "EIP712Domain struct hash mismatch");
+ }
+}
+"#,
+ )
+ .unwrap();
+
+ cmd.forge_fuse().args(["test", "--mc", "Eip712HashStructDomainTest", "-vvvv"]).assert_success();
+});
+
+forgetest!(test_eip712_hash_struct_from_bindings, |prj, cmd| {
prj.add_source(
"Eip712Permit.sol",
r#"
From 7223178dc865cefa599d5eb2e4cc944683cecb05 Mon Sep 17 00:00:00 2001
From: 0xrusowsky <0xrusowsky@proton.me>
Date: Tue, 27 May 2025 22:57:57 -0500
Subject: [PATCH 04/10] support for abi-encoded data
---
crates/cheatcodes/assets/cheatcodes.json | 22 ++++++-
crates/cheatcodes/spec/src/vm.rs | 3 +
crates/cheatcodes/src/utils.rs | 81 ++++++++++++++++++++++--
crates/forge/tests/cli/test_cmd.rs | 52 +++++++++++++--
testdata/cheats/Vm.sol | 1 +
5 files changed, 147 insertions(+), 12 deletions(-)
diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json
index a3a8ee234b922..29dc2a0b92120 100644
--- a/crates/cheatcodes/assets/cheatcodes.json
+++ b/crates/cheatcodes/assets/cheatcodes.json
@@ -4298,6 +4298,26 @@
"func": {
"id": "eip712HashStruct_0",
"description": "",
+ "declaration": "function eip712HashStruct(string calldata typeNameOrDefinition, bytes calldata abiEncodedData) external pure returns (bytes32 typeHash);",
+ "visibility": "external",
+ "mutability": "pure",
+ "signature": "eip712HashStruct(string,bytes)",
+ "selector": "0xaedeaebc",
+ "selectorBytes": [
+ 174,
+ 222,
+ 174,
+ 188
+ ]
+ },
+ "group": "utilities",
+ "status": "stable",
+ "safety": "safe"
+ },
+ {
+ "func": {
+ "id": "eip712HashStruct_1",
+ "description": "",
"declaration": "function eip712HashStruct(string calldata typeNameOrDefinition, string calldata jsonData) external pure returns (bytes32 typeHash);",
"visibility": "external",
"mutability": "pure",
@@ -4316,7 +4336,7 @@
},
{
"func": {
- "id": "eip712HashStruct_1",
+ "id": "eip712HashStruct_2",
"description": "",
"declaration": "function eip712HashStruct(string calldata bindingsPath, string calldata typeName, string calldata jsonData) external pure returns (bytes32 typeHash);",
"visibility": "external",
diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs
index 11be740237c69..4288aa505f8f3 100644
--- a/crates/cheatcodes/spec/src/vm.rs
+++ b/crates/cheatcodes/spec/src/vm.rs
@@ -2911,6 +2911,9 @@ interface Vm {
#[cheatcode(group = Utilities)]
function eip712HashType(string calldata bindingsPath, string calldata typeName) external pure returns (bytes32 typeHash);
+ #[cheatcode(group = Utilities)]
+ function eip712HashStruct(string calldata typeNameOrDefinition, bytes calldata abiEncodedData) external pure returns (bytes32 typeHash);
+
#[cheatcode(group = Utilities)]
function eip712HashStruct(string calldata typeNameOrDefinition, string calldata jsonData) external pure returns (bytes32 typeHash);
diff --git a/crates/cheatcodes/src/utils.rs b/crates/cheatcodes/src/utils.rs
index 295d02b8fe224..207b1ef536a8a 100644
--- a/crates/cheatcodes/src/utils.rs
+++ b/crates/cheatcodes/src/utils.rs
@@ -2,7 +2,7 @@
use crate::{Cheatcode, Cheatcodes, CheatcodesExecutor, CheatsCtxt, Result, Vm::*};
use alloy_dyn_abi::{eip712_parser::EncodeType, DynSolType, DynSolValue, Resolver};
-use alloy_primitives::{aliases::B32, keccak256, map::HashMap, B64, U256};
+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;
@@ -340,23 +340,34 @@ impl Cheatcode for eip712HashType_1Call {
impl Cheatcode for eip712HashStruct_0Call {
fn apply(&self, state: &mut Cheatcodes) -> Result {
- let Self { typeNameOrDefinition, jsonData } = self;
+ 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, jsonData)
+ get_struct_hash_from_bytes(primary, &type_def, abiEncodedData)
}
}
impl Cheatcode for eip712HashStruct_1Call {
+ fn apply(&self, state: &mut Cheatcodes) -> Result {
+ let Self { typeNameOrDefinition, jsonData } = 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_from_json(primary, &type_def, jsonData)
+ }
+}
+
+impl Cheatcode for eip712HashStruct_2Call {
fn apply(&self, state: &mut Cheatcodes) -> Result {
let Self { bindingsPath, typeName, jsonData } = 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, jsonData)
+ get_struct_hash_from_json(typeName, &type_def, jsonData)
}
}
@@ -413,7 +424,7 @@ fn get_type_def_from_bindings(name: &String, path: PathBuf, root: &PathBuf) -> R
}
}
-fn get_struct_hash(primary: &str, type_def: &String, json_data: &str) -> Result {
+fn get_struct_hash_from_json(primary: &str, type_def: &String, json_data: &str) -> Result {
let mut resolver = Resolver::default();
// Populate the resolver by ingesting the canonical type definition, and then get the
@@ -458,3 +469,63 @@ fn get_struct_hash(primary: &str, type_def: &String, json_data: &str) -> Result
Ok(keccak256(&bytes_to_hash).to_vec())
}
+
+fn get_struct_hash_from_bytes(primary: &str, type_def: &String, bytes: &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 EIP712 primary type '{}' using resolver: {}", primary, e)
+ })?;
+
+ let (props, types) =
+ if let DynSolType::CustomStruct { name: _, prop_names, tuple } = &resolved_sol_type {
+ (prop_names.clone(), tuple.clone())
+ } else {
+ bail!(
+ "Primary type '{}' is not a CustomStruct in the EIP-712 definition. Actual type: {:?}",
+ primary,
+ resolved_sol_type
+ );
+ };
+
+ // Decode the ABI-encoded bytes and generate a `DynSolValue::CustomStruct`.
+ let decoded_abi_value = DynSolType::Tuple(types).abi_decode_sequence(bytes.as_ref())?;
+ let decoded_member_values = match decoded_abi_value {
+ DynSolValue::Tuple(members) => members,
+ other => bail!(
+ "ABI decoding for struct '{primary}' did not result in a Tuple as expected. Got: {other:?}",
+ ),
+ };
+
+ let sol_value = DynSolValue::CustomStruct {
+ name: primary.to_string(),
+ prop_names: props,
+ tuple: decoded_member_values,
+ };
+
+ // Use the resolver to properly encode the data.
+ let encoded_data: Vec = 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())
+}
diff --git a/crates/forge/tests/cli/test_cmd.rs b/crates/forge/tests/cli/test_cmd.rs
index d49bb6d9aab8c..02071594b2c76 100644
--- a/crates/forge/tests/cli/test_cmd.rs
+++ b/crates/forge/tests/cli/test_cmd.rs
@@ -3978,12 +3978,12 @@ bytes32 constant _PERMIT_SINGLE_TYPEHASH = keccak256(
// borrowed from https://github.com/Uniswap/permit2/blob/main/src/libraries/PermitHash.sol
library PermitHash {
function hash(PermitSingle memory permitSingle) internal pure returns (bytes32) {
- bytes32 permitHash = _hashPermitDetails(permitSingle.details);
+ bytes32 permitHash = _hashDetails(permitSingle.details);
return
keccak256(abi.encode(_PERMIT_SINGLE_TYPEHASH, permitHash, permitSingle.spender, permitSingle.sigDeadline));
}
- function _hashPermitDetails(PermitDetails memory details) private pure returns (bytes32) {
+ function _hashDetails(PermitDetails memory details) internal pure returns (bytes32) {
return keccak256(abi.encode(_PERMIT_DETAILS_TYPEHASH, details));
}
}
@@ -4007,12 +4007,11 @@ import "./console.sol";
import "./Eip712Permit.sol";
import {JsonBindings} from "utils/JsonBindings.sol";
-
contract Eip712HashStructTest is DSTest {
Vm constant vm = Vm(HEVM_ADDRESS);
using JsonBindings for *;
- function testHashPermitSingle() public {
+ function testHashJsonPermitSingle_jsonData() public {
PermitDetails memory details = PermitDetails({
token: 0x1111111111111111111111111111111111111111,
amount: 1000 ether,
@@ -4020,6 +4019,14 @@ contract Eip712HashStructTest is DSTest {
nonce: 1
});
+ // user-computed details (using uniswap hash library)
+ bytes32 userStructHash = PermitHash._hashDetails(details);
+
+ // cheatcode-computed details (previously serializing to JSON)
+ bytes32 cheatStructHash = vm.eip712HashStruct("PermitDetails", details.serialize());
+
+ assertEq(cheatStructHash, userStructHash, "details struct hash mismatch");
+
PermitSingle memory permit = PermitSingle({
details: details,
spender: 0x2222222222222222222222222222222222222222,
@@ -4027,7 +4034,7 @@ contract Eip712HashStructTest is DSTest {
});
// user-computed permit (using uniswap hash library)
- bytes32 userStructHash = PermitHash.hash(permit);
+ userStructHash = PermitHash.hash(permit);
// cheatcode-computed permit (previously serializing to JSON)
string memory jsonData = permit.serialize();
@@ -4035,7 +4042,40 @@ contract Eip712HashStructTest is DSTest {
console.log("JSON data for PermitSingle:");
console.log(jsonData);
- bytes32 cheatStructHash = vm.eip712HashStruct("PermitSingle", jsonData);
+ cheatStructHash = vm.eip712HashStruct("PermitSingle", jsonData);
+ console.log("PermitSingle struct hash from cheatcode:");
+ console.logBytes32(cheatStructHash);
+
+ assertEq(cheatStructHash, userStructHash, "permit struct hash mismatch");
+ }
+
+ function testHashPermitSingle_abiEncodedData() public {
+ PermitDetails memory details = PermitDetails({
+ token: 0x1111111111111111111111111111111111111111,
+ amount: 1000 ether,
+ expiration: 12345,
+ nonce: 1
+ });
+
+ // user-computed permit (using uniswap hash library)
+ bytes32 userStructHash = PermitHash._hashDetails(details);
+
+ // cheatcode-computed permit (previously encoding)
+ bytes32 cheatStructHash = vm.eip712HashStruct("PermitDetails", abi.encode(details));
+
+ assertEq(cheatStructHash, userStructHash, "details struct hash mismatch");
+
+ PermitSingle memory permit = PermitSingle({
+ details: details,
+ spender: 0x2222222222222222222222222222222222222222,
+ sigDeadline: 12345
+ });
+
+ // user-computed permit (using uniswap hash library)
+ userStructHash = PermitHash.hash(permit);
+
+ // cheatcode-computed permit (previously encoding)
+ cheatStructHash = vm.eip712HashStruct("PermitSingle", abi.encode(permit));
console.log("PermitSingle struct hash from cheatcode:");
console.logBytes32(cheatStructHash);
diff --git a/testdata/cheats/Vm.sol b/testdata/cheats/Vm.sol
index d7396d7b83916..597749e7b6bc3 100644
--- a/testdata/cheats/Vm.sol
+++ b/testdata/cheats/Vm.sol
@@ -208,6 +208,7 @@ interface Vm {
function deriveKey(string calldata mnemonic, string calldata derivationPath, uint32 index, string calldata language) external pure returns (uint256 privateKey);
function difficulty(uint256 newDifficulty) external;
function dumpState(string calldata pathToStateJson) external;
+ function eip712HashStruct(string calldata typeNameOrDefinition, bytes calldata abiEncodedData) external pure returns (bytes32 typeHash);
function eip712HashStruct(string calldata typeNameOrDefinition, string calldata jsonData) external pure returns (bytes32 typeHash);
function eip712HashStruct(string calldata bindingsPath, string calldata typeName, string calldata jsonData) external pure returns (bytes32 typeHash);
function eip712HashType(string calldata typeNameOrDefinition) external pure returns (bytes32 typeHash);
From e0bf50fdad975ad7c318476f14599f9163b019a4 Mon Sep 17 00:00:00 2001
From: 0xrusowsky <0xrusowsky@proton.me>
Date: Tue, 27 May 2025 23:05:45 -0500
Subject: [PATCH 05/10] style: clippy
---
crates/cheatcodes/src/utils.rs | 40 ++++++++++++++--------------------
1 file changed, 16 insertions(+), 24 deletions(-)
diff --git a/crates/cheatcodes/src/utils.rs b/crates/cheatcodes/src/utils.rs
index 207b1ef536a8a..5c668fbe3c2f5 100644
--- a/crates/cheatcodes/src/utils.rs
+++ b/crates/cheatcodes/src/utils.rs
@@ -433,34 +433,32 @@ fn get_struct_hash_from_json(primary: &str, type_def: &String, json_data: &str)
.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 EIP712 primary type '{}' using resolver: {}", primary, e)
- })?;
+ let resolved_sol_type = resolver
+ .resolve(primary)
+ .map_err(|e| fmt_err!("Failed to resolve EIP712 primary type '{primary}': {e}"))?;
// Coerce the JSON data into a `DynSolValue`.
let json_data = serde_json::from_str(json_data)
- .map_err(|e| fmt_err!("Failed to parse input JSON data: {}", e))?;
+ .map_err(|e| fmt_err!("Failed to parse input JSON data: {e}"))?;
let sol_value = resolved_sol_type.coerce_json(&json_data).map_err(|e| {
- fmt_err!("Failed to coerce JSON data to EIP-712 'SolValue' for type '{}': {}", primary, e)
+ fmt_err!("Failed to coerce JSON data to EIP-712 'SolValue' for type '{primary}': {e}")
})?;
if !matches!(sol_value, DynSolValue::CustomStruct { .. }) {
- bail!("JSON data for type '{}' is not a custom Struct", primary);
+ bail!("JSON data for type '{primary}' is not a custom struct");
}
// Use the resolver to properly encode the data.
let encoded_data: Vec = 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)
- })?;
+ .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))?;
+ .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());
@@ -479,19 +477,15 @@ fn get_struct_hash_from_bytes(primary: &str, type_def: &String, bytes: &Bytes) -
.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 EIP712 primary type '{}' using resolver: {}", primary, e)
- })?;
+ let resolved_sol_type = resolver
+ .resolve(primary)
+ .map_err(|e| fmt_err!("Failed to resolve EIP-712 primary type '{primary}': {e}"))?;
let (props, types) =
if let DynSolType::CustomStruct { name: _, prop_names, tuple } = &resolved_sol_type {
(prop_names.clone(), tuple.clone())
} else {
- bail!(
- "Primary type '{}' is not a CustomStruct in the EIP-712 definition. Actual type: {:?}",
- primary,
- resolved_sol_type
- );
+ bail!("Primary type '{primary}' is not a custom struct");
};
// Decode the ABI-encoded bytes and generate a `DynSolValue::CustomStruct`.
@@ -512,15 +506,13 @@ fn get_struct_hash_from_bytes(primary: &str, type_def: &String, bytes: &Bytes) -
// Use the resolver to properly encode the data.
let encoded_data: Vec = 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)
- })?;
+ .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))?;
+ .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());
From b144fad909364d01a7405dd3262e180f4f5dee79 Mon Sep 17 00:00:00 2001
From: 0xrusowsky <0xrusowsky@proton.me>
Date: Wed, 28 May 2025 08:45:02 -0500
Subject: [PATCH 06/10] always use abi-encoded data to simplify API
---
crates/cheatcodes/assets/cheatcodes.json | 38 ++----
crates/cheatcodes/spec/src/vm.rs | 22 +++-
crates/cheatcodes/src/utils.rs | 91 ++------------
crates/forge/tests/cli/test_cmd.rs | 154 ++++++++++++++++++-----
testdata/cheats/Vm.sol | 3 +-
5 files changed, 160 insertions(+), 148 deletions(-)
diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json
index 29dc2a0b92120..1cfbab6382cf5 100644
--- a/crates/cheatcodes/assets/cheatcodes.json
+++ b/crates/cheatcodes/assets/cheatcodes.json
@@ -4297,7 +4297,7 @@
{
"func": {
"id": "eip712HashStruct_0",
- "description": "",
+ "description": "Generates the struct hash of the canonical EIP-712 type representation and its abi-encoded data.\nSupports 2 different inputs:\n 1. Name of the type (i.e. \"PermitSingle\"):\n * requires previous binding generation with `forge bind-json`.\n * bindings will be retrieved from the path configured in `foundry.toml`.\n 2. String representation of the type (i.e. \"Foo(Bar bar) Bar(uint256 baz)\").\n * Note: the cheatcode will use the canonical type even if the input is malformated\n with the wrong order of elements or with extra whitespaces.",
"declaration": "function eip712HashStruct(string calldata typeNameOrDefinition, bytes calldata abiEncodedData) external pure returns (bytes32 typeHash);",
"visibility": "external",
"mutability": "pure",
@@ -4317,37 +4317,17 @@
{
"func": {
"id": "eip712HashStruct_1",
- "description": "",
- "declaration": "function eip712HashStruct(string calldata typeNameOrDefinition, string calldata jsonData) external pure returns (bytes32 typeHash);",
+ "description": "Generates the struct hash of the canonical EIP-712 type representation and its abi-encoded data.\nRequires previous binding generation with `forge bind-json`.\nParams:\n * `bindingsPath`: path where the output of `forge bind-json` is stored.\n * `typeName`: Name of the type (i.e. \"PermitSingle\").\n * `abiEncodedData`: ABI-encoded data for the struct that is being hashed.",
+ "declaration": "function eip712HashStruct(string calldata bindingsPath, string calldata typeName, bytes calldata abiEncodedData) external pure returns (bytes32 typeHash);",
"visibility": "external",
"mutability": "pure",
- "signature": "eip712HashStruct(string,string)",
- "selector": "0x686230fc",
+ "signature": "eip712HashStruct(string,string,bytes)",
+ "selector": "0x6d06c57c",
"selectorBytes": [
- 104,
- 98,
- 48,
- 252
- ]
- },
- "group": "utilities",
- "status": "stable",
- "safety": "safe"
- },
- {
- "func": {
- "id": "eip712HashStruct_2",
- "description": "",
- "declaration": "function eip712HashStruct(string calldata bindingsPath, string calldata typeName, string calldata jsonData) external pure returns (bytes32 typeHash);",
- "visibility": "external",
- "mutability": "pure",
- "signature": "eip712HashStruct(string,string,string)",
- "selector": "0x0e7d8d44",
- "selectorBytes": [
- 14,
- 125,
- 141,
- 68
+ 109,
+ 6,
+ 197,
+ 124
]
},
"group": "utilities",
diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs
index 4288aa505f8f3..75efabfd53302 100644
--- a/crates/cheatcodes/spec/src/vm.rs
+++ b/crates/cheatcodes/spec/src/vm.rs
@@ -2911,14 +2911,28 @@ interface Vm {
#[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 typeNameOrDefinition, string calldata jsonData) external pure returns (bytes32 typeHash);
-
- #[cheatcode(group = Utilities)]
- function eip712HashStruct(string calldata bindingsPath, string calldata typeName, string calldata jsonData) external pure returns (bytes32 typeHash);
+ function eip712HashStruct(string calldata bindingsPath, string calldata typeName, bytes calldata abiEncodedData) external pure returns (bytes32 typeHash);
}
}
diff --git a/crates/cheatcodes/src/utils.rs b/crates/cheatcodes/src/utils.rs
index 5c668fbe3c2f5..a699e52c9642d 100644
--- a/crates/cheatcodes/src/utils.rs
+++ b/crates/cheatcodes/src/utils.rs
@@ -345,29 +345,18 @@ impl Cheatcode for eip712HashStruct_0Call {
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_from_bytes(primary, &type_def, abiEncodedData)
+ get_struct_hash(primary, &type_def, abiEncodedData)
}
}
impl Cheatcode for eip712HashStruct_1Call {
fn apply(&self, state: &mut Cheatcodes) -> Result {
- let Self { typeNameOrDefinition, jsonData } = 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_from_json(primary, &type_def, jsonData)
- }
-}
-
-impl Cheatcode for eip712HashStruct_2Call {
- fn apply(&self, state: &mut Cheatcodes) -> Result {
- let Self { bindingsPath, typeName, jsonData } = self;
+ 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_from_json(typeName, &type_def, jsonData)
+ get_struct_hash(typeName, &type_def, abiEncodedData)
}
}
@@ -424,51 +413,7 @@ fn get_type_def_from_bindings(name: &String, path: PathBuf, root: &PathBuf) -> R
}
}
-fn get_struct_hash_from_json(primary: &str, type_def: &String, json_data: &str) -> 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 EIP712 primary type '{primary}': {e}"))?;
-
- // Coerce the JSON data into a `DynSolValue`.
- let json_data = serde_json::from_str(json_data)
- .map_err(|e| fmt_err!("Failed to parse input JSON data: {e}"))?;
-
- let sol_value = resolved_sol_type.coerce_json(&json_data).map_err(|e| {
- fmt_err!("Failed to coerce JSON data to EIP-712 'SolValue' for type '{primary}': {e}")
- })?;
-
- if !matches!(sol_value, DynSolValue::CustomStruct { .. }) {
- bail!("JSON data for type '{primary}' is not a custom struct");
- }
-
- // Use the resolver to properly encode the data.
- let encoded_data: Vec = 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())
-}
-
-fn get_struct_hash_from_bytes(primary: &str, type_def: &String, bytes: &Bytes) -> Result {
+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
@@ -481,27 +426,13 @@ fn get_struct_hash_from_bytes(primary: &str, type_def: &String, bytes: &Bytes) -
.resolve(primary)
.map_err(|e| fmt_err!("Failed to resolve EIP-712 primary type '{primary}': {e}"))?;
- let (props, types) =
- if let DynSolType::CustomStruct { name: _, prop_names, tuple } = &resolved_sol_type {
- (prop_names.clone(), tuple.clone())
- } else {
- bail!("Primary type '{primary}' is not a custom struct");
- };
-
- // Decode the ABI-encoded bytes and generate a `DynSolValue::CustomStruct`.
- let decoded_abi_value = DynSolType::Tuple(types).abi_decode_sequence(bytes.as_ref())?;
- let decoded_member_values = match decoded_abi_value {
- DynSolValue::Tuple(members) => members,
- other => bail!(
- "ABI decoding for struct '{primary}' did not result in a Tuple as expected. Got: {other:?}",
- ),
- };
-
- let sol_value = DynSolValue::CustomStruct {
- name: primary.to_string(),
- prop_names: props,
- tuple: decoded_member_values,
- };
+ // ABI-decode the bytes into `DynSolType::Tuple` and convert to `DynSolValue::CustomStruct`.
+ let sol_value = resolved_sol_type.abi_decode(abi_encoded_data.as_ref()).map_err(|e| {
+ fmt_err!(
+ "Decoding '{primary}'. Data (length: {}): {abi_encoded_data}.\nFailed to ABI decode using resolved_sol_type directly for '{primary}': {e}.\n\nresolved_sol_type: {resolved_sol_type:?}",
+ abi_encoded_data.len()
+ )
+ })?;
// Use the resolver to properly encode the data.
let encoded_data: Vec = resolver
diff --git a/crates/forge/tests/cli/test_cmd.rs b/crates/forge/tests/cli/test_cmd.rs
index 02071594b2c76..c3c52d117a7c2 100644
--- a/crates/forge/tests/cli/test_cmd.rs
+++ b/crates/forge/tests/cli/test_cmd.rs
@@ -3884,7 +3884,7 @@ Encountered a total of 1 failing tests, 0 tests succeeded
.assert_success();
});
-forgetest!(test_eip712_hash_struct_simple_domain, |prj, cmd| {
+forgetest!(test_eip712_hash_struct_simple, |prj, cmd| {
prj.insert_ds_test();
prj.insert_vm();
prj.insert_console();
@@ -3917,7 +3917,7 @@ contract Eip712HashStructDomainTest is DSTest {
verifyingContract: 0xdEADBEeF00000000000000000000000000000000
});
- // simulate user-computed hash
+ // simulate user-computed domain hash
bytes memory encodedData = abi.encode(
keccak256(bytes(domain.name)),
keccak256(bytes(domain.version)),
@@ -3926,15 +3926,8 @@ contract Eip712HashStructDomainTest is DSTest {
);
bytes32 userStructHash = keccak256(abi.encodePacked(_EIP712_DOMAIN_TYPE_HASH, encodedData));
- // cheatcode-computed permit (manually serializing to JSON)
- string memory jsonData = vm.serializeString("domain", "name", domain.name);
- jsonData = vm.serializeString("domain", "version", domain.version);
- jsonData = vm.serializeUint("domain", "chainId", domain.chainId);
- jsonData = vm.serializeAddress("domain", "verifyingContract", domain.verifyingContract);
- console.log("JSON Data for EIP712Domain:");
- console.log(jsonData);
-
- bytes32 cheatStructHash = vm.eip712HashStruct(_EIP712_DOMAIN_TYPE_DEF, jsonData);
+ // cheatcode-computed domain hash
+ bytes32 cheatStructHash = vm.eip712HashStruct(_EIP712_DOMAIN_TYPE_DEF, abi.encode(domain));
console.log("EIP712Domain struct hash from cheatcode:");
console.logBytes32(cheatStructHash);
@@ -3948,7 +3941,7 @@ contract Eip712HashStructDomainTest is DSTest {
cmd.forge_fuse().args(["test", "--mc", "Eip712HashStructDomainTest", "-vvvv"]).assert_success();
});
-forgetest!(test_eip712_hash_struct_from_bindings, |prj, cmd| {
+forgetest!(test_eip712_hash_struct_complex, |prj, cmd| {
prj.add_source(
"Eip712Permit.sol",
r#"
@@ -3959,7 +3952,6 @@ struct PermitDetails {
uint48 nonce;
}
-// Canonical type hash for PermitDetails
bytes32 constant _PERMIT_DETAILS_TYPEHASH = keccak256(
"PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)"
);
@@ -3970,7 +3962,6 @@ struct PermitSingle {
uint256 sigDeadline;
}
-// Canonical type hash for PermitSingle
bytes32 constant _PERMIT_SINGLE_TYPEHASH = keccak256(
"PermitSingle(PermitDetails details,address spender,uint256 sigDeadline)PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)"
);
@@ -3991,6 +3982,62 @@ library PermitHash {
)
.unwrap();
+ prj.add_source(
+ "Eip712Transaction.sol",
+ r#"
+struct Asset {
+ address token;
+ uint256 amount;
+}
+
+bytes32 constant _ASSET_TYPEHASH = keccak256(
+ "Asset(address token,uint256 amount)"
+);
+
+struct Person {
+ address wallet;
+ string name;
+}
+
+bytes32 constant _PERSON_TYPEHASH = keccak256(
+ "Person(address wallet,string name)"
+);
+
+struct Transaction {
+ Person from;
+ Person to;
+ Asset tx;
+}
+
+bytes32 constant _TRANSACTION_TYPEHASH = keccak256(
+ "Transaction(Person from,Person to,Asset tx)Asset(address token,uint256 amount)Person(address wallet,string name)"
+);
+
+
+library TransactionHash {
+ function hash(Transaction memory t) internal pure returns (bytes32) {
+ bytes32 fromHash = _hashPerson(t.from);
+ bytes32 toHash = _hashPerson(t.to);
+ bytes32 assetHash = _hashAsset(t.tx);
+ return
+ keccak256(abi.encode(_TRANSACTION_TYPEHASH, fromHash, toHash, assetHash));
+ }
+
+ function _hashPerson(Person memory person) internal pure returns (bytes32) {
+ return keccak256(
+ abi.encode(_PERSON_TYPEHASH, person.wallet, keccak256(bytes(person.name)))
+ );
+
+ }
+
+ function _hashAsset(Asset memory asset) internal pure returns (bytes32) {
+ return keccak256(abi.encode(_ASSET_TYPEHASH, asset));
+ }
+}
+ "#,
+ )
+ .unwrap();
+
let bindings = prj.root().join("utils").join("JsonBindings.sol");
prj.update_config(|config| config.fs_permissions.add(PathPermission::read(&bindings)));
cmd.forge_fuse().args(["bind-json"]).assert_success();
@@ -4005,13 +4052,12 @@ import "./Vm.sol";
import "./test.sol";
import "./console.sol";
import "./Eip712Permit.sol";
-import {JsonBindings} from "utils/JsonBindings.sol";
+import "./Eip712Transaction.sol";
contract Eip712HashStructTest is DSTest {
Vm constant vm = Vm(HEVM_ADDRESS);
- using JsonBindings for *;
- function testHashJsonPermitSingle_jsonData() public {
+ function testHashPermitSingle_withTypeName() public {
PermitDetails memory details = PermitDetails({
token: 0x1111111111111111111111111111111111111111,
amount: 1000 ether,
@@ -4019,11 +4065,11 @@ contract Eip712HashStructTest is DSTest {
nonce: 1
});
- // user-computed details (using uniswap hash library)
+ // user-computed permit (using uniswap hash library)
bytes32 userStructHash = PermitHash._hashDetails(details);
- // cheatcode-computed details (previously serializing to JSON)
- bytes32 cheatStructHash = vm.eip712HashStruct("PermitDetails", details.serialize());
+ // cheatcode-computed permit
+ bytes32 cheatStructHash = vm.eip712HashStruct("PermitDetails", abi.encode(details));
assertEq(cheatStructHash, userStructHash, "details struct hash mismatch");
@@ -4036,20 +4082,15 @@ contract Eip712HashStructTest is DSTest {
// user-computed permit (using uniswap hash library)
userStructHash = PermitHash.hash(permit);
- // cheatcode-computed permit (previously serializing to JSON)
- string memory jsonData = permit.serialize();
-
- console.log("JSON data for PermitSingle:");
- console.log(jsonData);
-
- cheatStructHash = vm.eip712HashStruct("PermitSingle", jsonData);
+ // cheatcode-computed permit
+ cheatStructHash = vm.eip712HashStruct("PermitSingle", abi.encode(permit));
console.log("PermitSingle struct hash from cheatcode:");
console.logBytes32(cheatStructHash);
assertEq(cheatStructHash, userStructHash, "permit struct hash mismatch");
}
- function testHashPermitSingle_abiEncodedData() public {
+ function testHashPermitSingle_withTypeDefinion() public {
PermitDetails memory details = PermitDetails({
token: 0x1111111111111111111111111111111111111111,
amount: 1000 ether,
@@ -4060,8 +4101,8 @@ contract Eip712HashStructTest is DSTest {
// user-computed permit (using uniswap hash library)
bytes32 userStructHash = PermitHash._hashDetails(details);
- // cheatcode-computed permit (previously encoding)
- bytes32 cheatStructHash = vm.eip712HashStruct("PermitDetails", abi.encode(details));
+ // cheatcode-computed permit
+ bytes32 cheatStructHash = vm.eip712HashStruct("PermitDetails(address token, uint160 amount, uint48 expiration, uint48 nonce)", abi.encode(details));
assertEq(cheatStructHash, userStructHash, "details struct hash mismatch");
@@ -4075,12 +4116,54 @@ contract Eip712HashStructTest is DSTest {
userStructHash = PermitHash.hash(permit);
// cheatcode-computed permit (previously encoding)
- cheatStructHash = vm.eip712HashStruct("PermitSingle", abi.encode(permit));
+ cheatStructHash = vm.eip712HashStruct("PermitDetails(address token, uint160 amount, uint48 expiration, uint48 nonce) PermitSingle(PermitDetails details,address spender,uint256 sigDeadline)", abi.encode(permit));
console.log("PermitSingle struct hash from cheatcode:");
console.logBytes32(cheatStructHash);
assertEq(cheatStructHash, userStructHash, "permit struct hash mismatch");
}
+
+ function testHashTransaction_withTypeName() public {
+ Asset memory asset = Asset ({ token: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, amount: 100 ether });
+
+ bytes32 user = TransactionHash._hashAsset(asset);
+ bytes32 cheat = vm.eip712HashStruct("Asset", abi.encode(asset));
+ assertEq(user, cheat, "asset struct hash mismatch");
+
+ Person memory from = Person ({ wallet: 0x0000000000000000000000000000000000000001, name: "alice" });
+ Person memory to = Person ({ wallet: 0x0000000000000000000000000000000000000002, name: "bob" });
+
+ user = TransactionHash._hashPerson(from);
+ cheat = vm.eip712HashStruct("Person", abi.encode(from));
+ assertEq(user, cheat, "person struct hash mismatch");
+
+ Transaction memory t = Transaction ({ from: from, to: to, tx: asset });
+
+ user = TransactionHash.hash(t);
+ cheat = vm.eip712HashStruct("Transaction", abi.encode(t));
+ assertEq(user, cheat, "transaction struct hash mismatch");
+ }
+
+ function testHashTransaction_withTypeDefinition() public {
+ Asset memory asset = Asset ({ token: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, amount: 100 ether });
+
+ bytes32 user = TransactionHash._hashAsset(asset);
+ bytes32 cheat = vm.eip712HashStruct("Asset(address token, uint256 amount)", abi.encode(asset));
+ assertEq(user, cheat, "asset struct hash mismatch");
+
+ Person memory from = Person ({ wallet: 0x0000000000000000000000000000000000000001, name: "alice" });
+ Person memory to = Person ({ wallet: 0x0000000000000000000000000000000000000002, name: "bob" });
+
+ user = TransactionHash._hashPerson(from);
+ cheat = vm.eip712HashStruct("Person(address wallet, string name)", abi.encode(from));
+ assertEq(user, cheat, "person struct hash mismatch");
+
+ Transaction memory t = Transaction ({ from: from, to: to, tx: asset });
+
+ user = TransactionHash.hash(t);
+ cheat = vm.eip712HashStruct("Person(address wallet, string name) Asset(address token, uint256 amount) Transaction(Person from, Person to, Asset tx)", abi.encode(t));
+ assertEq(user, cheat, "transaction struct hash mismatch");
+ }
}
"#,
)
@@ -4091,8 +4174,13 @@ contract Eip712HashStructTest is DSTest {
.assert_success()
.stdout_eq(str![[r#"
...
- JSON data for PermitSingle:
- {"details":{"token":"0x1111111111111111111111111111111111111111","amount":"1000000000000000000000","expiration":12345,"nonce":1},"spender":"0x2222222222222222222222222222222222222222","sigDeadline":12345}
+[PASS] testHashPermitSingle_withTypeDefinion() ([GAS])
+Logs:
+ PermitSingle struct hash from cheatcode:
+ 0x3ed744fdcea02b6b9ad45a9db6e648bf6f18c221909f9ee425191f2a02f9e4a8
+
+[PASS] testHashPermitSingle_withTypeName() ([GAS])
+Logs:
PermitSingle struct hash from cheatcode:
0x3ed744fdcea02b6b9ad45a9db6e648bf6f18c221909f9ee425191f2a02f9e4a8
...
diff --git a/testdata/cheats/Vm.sol b/testdata/cheats/Vm.sol
index 597749e7b6bc3..9a62a45254576 100644
--- a/testdata/cheats/Vm.sol
+++ b/testdata/cheats/Vm.sol
@@ -209,8 +209,7 @@ interface Vm {
function difficulty(uint256 newDifficulty) external;
function dumpState(string calldata pathToStateJson) external;
function eip712HashStruct(string calldata typeNameOrDefinition, bytes calldata abiEncodedData) external pure returns (bytes32 typeHash);
- function eip712HashStruct(string calldata typeNameOrDefinition, string calldata jsonData) external pure returns (bytes32 typeHash);
- function eip712HashStruct(string calldata bindingsPath, string calldata typeName, string calldata jsonData) external pure returns (bytes32 typeHash);
+ function eip712HashStruct(string calldata bindingsPath, string calldata typeName, bytes calldata abiEncodedData) external pure returns (bytes32 typeHash);
function eip712HashType(string calldata typeNameOrDefinition) external pure returns (bytes32 typeHash);
function eip712HashType(string calldata bindingsPath, string calldata typeName) external pure returns (bytes32 typeHash);
function ensNamehash(string calldata name) external pure returns (bytes32);
From e0b30f7ffcdabaaa56d34641017f92ccb3b8ea97 Mon Sep 17 00:00:00 2001
From: 0xrusowsky <0xrusowsky@proton.me>
Date: Wed, 28 May 2025 08:58:12 -0500
Subject: [PATCH 07/10] fix: revert json serialization changes
---
crates/cheatcodes/src/json.rs | 14 +++-----------
1 file changed, 3 insertions(+), 11 deletions(-)
diff --git a/crates/cheatcodes/src/json.rs b/crates/cheatcodes/src/json.rs
index 78c1febfdc89c..c5dcd1772a705 100644
--- a/crates/cheatcodes/src/json.rs
+++ b/crates/cheatcodes/src/json.rs
@@ -605,22 +605,14 @@ fn serialize_value_as_json(value: DynSolValue) -> Result {
DynSolValue::Bytes(b) => Ok(Value::String(hex::encode_prefixed(b))),
DynSolValue::FixedBytes(b, size) => Ok(Value::String(hex::encode_prefixed(&b[..size]))),
DynSolValue::Int(i, _) => {
- // TODO: same problem as with Uint
// let serde handle number parsing
let n = serde_json::from_str(&i.to_string())?;
Ok(Value::Number(n))
}
DynSolValue::Uint(i, _) => {
- // TODO: ask Arsenii if this approach (or always using strings) is acceptable.
- // needed cause otherwise alloy fails to coerce numbers > u64.
- // alternatively i could do an alloy PR.
- if i <= U256::from(u64::MAX) {
- // let serde handle number parsing
- let n = serde_json::from_str(&i.to_string())?;
- Ok(Value::Number(n))
- } else {
- Ok(Value::String(i.to_string()))
- }
+ // let serde handle number parsing
+ let n = serde_json::from_str(&i.to_string())?;
+ Ok(Value::Number(n))
}
DynSolValue::Address(a) => Ok(Value::String(a.to_string())),
DynSolValue::Array(e) | DynSolValue::FixedArray(e) => {
From 99fe63891c23c3619ccfa7760997cb9719a9df07 Mon Sep 17 00:00:00 2001
From: 0xrusowsky <0xrusowsky@proton.me>
Date: Wed, 28 May 2025 09:00:41 -0500
Subject: [PATCH 08/10] style: clippy
---
crates/cheatcodes/src/json.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/crates/cheatcodes/src/json.rs b/crates/cheatcodes/src/json.rs
index c5dcd1772a705..6ad36e4742901 100644
--- a/crates/cheatcodes/src/json.rs
+++ b/crates/cheatcodes/src/json.rs
@@ -2,7 +2,7 @@
use crate::{string, Cheatcode, Cheatcodes, Result, Vm::*};
use alloy_dyn_abi::{eip712_parser::EncodeType, DynSolType, DynSolValue, Resolver};
-use alloy_primitives::{hex, Address, B256, I256, U256};
+use alloy_primitives::{hex, Address, B256, I256};
use alloy_sol_types::SolValue;
use foundry_common::fs;
use foundry_config::fs_permissions::FsAccessKind;
From d0a6b286ed473d2884690d77583af92f7cc32932 Mon Sep 17 00:00:00 2001
From: 0xrusowsky <0xrusowsky@proton.me>
Date: Wed, 28 May 2025 09:03:19 -0500
Subject: [PATCH 09/10] style: comments
---
crates/cheatcodes/src/utils.rs | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/crates/cheatcodes/src/utils.rs b/crates/cheatcodes/src/utils.rs
index a699e52c9642d..3ea6a59bf28c5 100644
--- a/crates/cheatcodes/src/utils.rs
+++ b/crates/cheatcodes/src/utils.rs
@@ -426,13 +426,13 @@ fn get_struct_hash(primary: &str, type_def: &String, abi_encoded_data: &Bytes) -
.resolve(primary)
.map_err(|e| fmt_err!("Failed to resolve EIP-712 primary type '{primary}': {e}"))?;
- // ABI-decode the bytes into `DynSolType::Tuple` and convert to `DynSolValue::CustomStruct`.
+ // 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!(
- "Decoding '{primary}'. Data (length: {}): {abi_encoded_data}.\nFailed to ABI decode using resolved_sol_type directly for '{primary}': {e}.\n\nresolved_sol_type: {resolved_sol_type:?}",
- abi_encoded_data.len()
- )
- })?;
+ fmt_err!(
+ "Failed to ABI decode using resolved_sol_type directly for '{primary}': {e}.",
+ abi_encoded_data.len()
+ )
+ })?;
// Use the resolver to properly encode the data.
let encoded_data: Vec = resolver
From db3ec06f665d4f5cf5f98e54328542d8c485eac5 Mon Sep 17 00:00:00 2001
From: 0xrusowsky <0xrusowsky@proton.me>
Date: Wed, 28 May 2025 09:03:39 -0500
Subject: [PATCH 10/10] fix: typo
---
crates/cheatcodes/src/utils.rs | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/crates/cheatcodes/src/utils.rs b/crates/cheatcodes/src/utils.rs
index 3ea6a59bf28c5..8cc78a5d91057 100644
--- a/crates/cheatcodes/src/utils.rs
+++ b/crates/cheatcodes/src/utils.rs
@@ -428,10 +428,7 @@ fn get_struct_hash(primary: &str, type_def: &String, abi_encoded_data: &Bytes) -
// 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}.",
- abi_encoded_data.len()
- )
+ fmt_err!("Failed to ABI decode using resolved_sol_type directly for '{primary}': {e}.")
})?;
// Use the resolver to properly encode the data.