diff --git a/Cargo.lock b/Cargo.lock index 583a27613a16d..bb57a8356ec4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3248,7 +3248,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3367,7 +3367,7 @@ checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", "rustix 1.0.5", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -5354,7 +5354,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi 0.5.0", "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -5426,7 +5426,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -7044,7 +7044,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -7584,7 +7584,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -7597,7 +7597,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -8336,7 +8336,7 @@ dependencies = [ "solar-config", "solar-data-structures", "solar-macros", - "thiserror 2.0.12", + "thiserror 1.0.69", "tracing", "unicode-width 0.2.0", ] @@ -8682,7 +8682,7 @@ dependencies = [ "serde_json", "sha2", "tempfile", - "thiserror 2.0.12", + "thiserror 1.0.69", "url", "zip", ] @@ -8780,7 +8780,7 @@ dependencies = [ "getrandom 0.3.2", "once_cell", "rustix 1.0.5", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -10008,7 +10008,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json index 59c5bab3cb457..19ebcbacf42a9 100644 --- a/crates/cheatcodes/assets/cheatcodes.json +++ b/crates/cheatcodes/assets/cheatcodes.json @@ -4274,6 +4274,26 @@ "status": "stable", "safety": "unsafe" }, + { + "func": { + "id": "eip712HashType", + "description": "", + "declaration": "function eip712HashType(string memory typeDefinition) external pure returns (bytes32 typeHash);", + "visibility": "external", + "mutability": "pure", + "signature": "eip712HashType(string)", + "selector": "0x6792e9e2", + "selectorBytes": [ + 103, + 146, + 233, + 226 + ] + }, + "group": "utilities", + "status": "stable", + "safety": "safe" + }, { "func": { "id": "ensNamehash", @@ -11043,4 +11063,4 @@ "safety": "safe" } ] -} \ No newline at end of file +} diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index 00f3b8004a96c..286248a47dfab 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -2872,6 +2872,9 @@ interface Vm { /// catch (bytes memory interceptedInitcode) { initcode = interceptedInitcode; } #[cheatcode(group = Utilities, safety = Unsafe)] function interceptInitcode() external; + + #[cheatcode(group = Utilities)] + function eip712HashType(string memory typeDefinition) external pure returns (bytes32 typeHash); } } diff --git a/crates/cheatcodes/src/utils.rs b/crates/cheatcodes/src/utils.rs index 4735ffaa41087..a3284b47e8f78 100644 --- a/crates/cheatcodes/src/utils.rs +++ b/crates/cheatcodes/src/utils.rs @@ -1,8 +1,13 @@ //! Implementations of [`Utilities`](spec::Group::Utilities) cheatcodes. +use std::collections::HashSet; + use crate::{Cheatcode, Cheatcodes, CheatcodesExecutor, CheatsCtxt, Result, Vm::*}; -use alloy_dyn_abi::{DynSolType, DynSolValue}; -use alloy_primitives::{aliases::B32, map::HashMap, B64, U256}; +use alloy_dyn_abi::{ + eip712_parser::{self, EncodeType}, + DynSolType, DynSolValue, +}; +use alloy_primitives::{aliases::B32, keccak256, map::HashMap, B64, U256}; use alloy_sol_types::SolValue; use foundry_common::ens::namehash; use foundry_evm_core::constants::DEFAULT_CREATE2_DEPLOYER; @@ -313,3 +318,91 @@ fn random_int(state: &mut Cheatcodes, bits: Option) -> Result { .current() .abi_encode()) } + +impl Cheatcode for eip712HashTypeCall { + fn apply(&self, _state: &mut Cheatcodes) -> Result { + let Self { typeDefinition } = self; + + let types = eip712_parser::EncodeType::parse(typeDefinition).map_err(|e| { + fmt_err!("Failed to parse EIP-712 type definition '{}': {}", typeDefinition, e) + })?; + + let canonical = canonicalize(types).map_err(|e| { + fmt_err!("Failed to canonicalize EIP-712 type definition: '{}': {}", typeDefinition, e) + })?; + let canonical_hash = keccak256(canonical.as_bytes()); + + Ok(canonical_hash.to_vec()) + } +} + +// TODO: replace for the built-in alloy fn when `https://github.com/alloy-rs/core/pull/950` is merged. +/// Computes the canonical string representation of the type. +/// +/// Orders the `ComponentTypes` based on the EIP712 rules, and removes unsupported whitespaces. +fn canonicalize(input: EncodeType) -> Result { + if input.types.is_empty() { + return Err("EIP-712 requires a primary type".into()); + } + + let primary_idx = get_primary_idx(&input)?; + + // EIP712 requires alphabeting order of the secondary types + let mut types = input.types.clone(); + let mut sorted = vec![types.remove(primary_idx)]; + types.sort_by(|a, b| a.type_name.cmp(b.type_name)); + sorted.extend(types); + + // Ensure no unintended whitespaces + Ok(sorted.into_iter().map(|t| t.span.trim().replace(", ", ",")).collect()) +} + +/// Identifies the primary type from the list of component types. +/// +/// The primary type is the component type that is not used as a property in any component type +/// definition within this set. +fn get_primary_idx(input: &EncodeType) -> Result { + // Track all defined component types and types used in component properties. + let mut components = HashSet::new(); + let mut types_in_props = HashSet::new(); + + for ty in &input.types { + components.insert(ty.type_name); + + for prop_def in &ty.props { + // Extract the base type name, removing array suffixes like "Person[]" + let type_str = prop_def.ty.span.trim(); + let type_str = type_str.split('[').next().unwrap_or(type_str).trim(); + + // A type is considered a reference to another type if its name starts with an + // uppercase letter, otherwise it is assumed to be a basic type + if !type_str.is_empty() && + type_str.chars().next().is_some_and(|c| c.is_ascii_uppercase()) + { + types_in_props.insert(type_str); + } + } + } + + // Ensure all types in props have a defined `ComponentType` + for ty in &types_in_props { + if !components.contains(ty) { + return Err(format!("missing component definition for '{ty}'")); + } + } + + // The primary type won't be a property of any other component + let mut primary = 0; + let mut is_found = false; + for (n, ty) in input.types.iter().enumerate() { + if !types_in_props.contains(ty.type_name) { + if is_found { + return Err("no primary component".into()); + } + primary = n; + is_found = true; + } + } + + Ok(primary) +} diff --git a/docs/dev/cheatcodes.md b/docs/dev/cheatcodes.md index 0815ca66bef50..0c96c4ba7c7f5 100644 --- a/docs/dev/cheatcodes.md +++ b/docs/dev/cheatcodes.md @@ -155,7 +155,7 @@ update of the files. 2. Implement the cheatcode in [`cheatcodes`] in its category's respective module. Follow the existing implementations as a guide. 3. If a struct, enum, error, or event was added to `Vm`, update [`spec::Cheatcodes::new`] 4. Update the JSON interface by running `cargo cheats` twice. This is expected to fail the first time that this is run after adding a new cheatcode; see [JSON interface](#json-interface) -5. Write an integration test for the cheatcode in [`testdata/cheats/`] +5. Write an integration test for the cheatcode in [`testdata/default/cheats/`] [`sol!`]: https://docs.rs/alloy-sol-macro/latest/alloy_sol_macro/macro.sol.html [`cheatcodes/spec/src/vm.rs`]: ../../crates/cheatcodes/spec/src/vm.rs diff --git a/testdata/cheats/Vm.sol b/testdata/cheats/Vm.sol index adf6733ba9424..85c9db04cd5a9 100644 --- a/testdata/cheats/Vm.sol +++ b/testdata/cheats/Vm.sol @@ -207,6 +207,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 eip712HashType(string memory typeDefinition) external pure returns (bytes32 typeHash); function ensNamehash(string calldata name) external pure returns (bytes32); function envAddress(string calldata name) external view returns (address value); function envAddress(string calldata name, string calldata delim) external view returns (address[] memory value); diff --git a/testdata/default/cheats/EIP712Hash.t.sol b/testdata/default/cheats/EIP712Hash.t.sol new file mode 100644 index 0000000000000..ddccefb305677 --- /dev/null +++ b/testdata/default/cheats/EIP712Hash.t.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.18; + +import "ds-test/test.sol"; +import "cheats/Vm.sol"; + +contract EIP712HashTypeCall is DSTest { + Vm constant vm = Vm(HEVM_ADDRESS); + bytes32 typeHash; + + // CANONICAL TYPES + bytes32 public constant _PERMIT_DETAILS_TYPEHASH = keccak256( + "PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)" + ); + bytes32 public constant _PERMIT_SINGLE_TYPEHASH = keccak256( + "PermitSingle(PermitDetails details,address spender,uint256 sigDeadline)PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)" + ); + bytes32 public constant _PERMIT_BATCH_TRANSFER_FROM_TYPEHASH = keccak256( + "PermitBatchTransferFrom(TokenPermissions[] permitted,address spender,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)" + ); + + function test_canHashCanonicalTypes() public { + typeHash = vm.eip712HashType("PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)"); + assertEq(typeHash, _PERMIT_DETAILS_TYPEHASH); + + typeHash = vm.eip712HashType( + "PermitSingle(PermitDetails details,address spender,uint256 sigDeadline)PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)" + ); + assertEq(typeHash, _PERMIT_SINGLE_TYPEHASH); + + typeHash = vm.eip712HashType( + "PermitBatchTransferFrom(TokenPermissions[] permitted,address spender,uint256 nonce,uint256 deadline)TokenPermissions(address token,uint256 amount)" + ); + assertEq(typeHash, _PERMIT_BATCH_TRANSFER_FROM_TYPEHASH); + } + + function test_canHashMessyTypes() public { + typeHash = vm.eip712HashType("PermitDetails(address token, uint160 amount, uint48 expiration, uint48 nonce)"); + assertEq(typeHash, _PERMIT_DETAILS_TYPEHASH); + + typeHash = vm.eip712HashType( + "PermitDetails(address token, uint160 amount, uint48 expiration, uint48 nonce) PermitSingle(PermitDetails details, address spender, uint256 sigDeadline)" + ); + assertEq(typeHash, _PERMIT_SINGLE_TYPEHASH); + + typeHash = vm.eip712HashType( + "TokenPermissions(address token, uint256 amount) PermitBatchTransferFrom(TokenPermissions[] permitted, address spender, uint256 nonce, uint256 deadline)" + ); + assertEq(typeHash, _PERMIT_BATCH_TRANSFER_FROM_TYPEHASH); + } + + function testRevert_cannotHashTypesWithMissingComponents() public { + vm._expectCheatcodeRevert(); + typeHash = vm.eip712HashType( + "PermitSingle(PermitDetails details, address spender, uint256 sigDeadline)" + ); + } +}