Skip to content

feat: eip712 type hash #10483

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

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
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
22 changes: 11 additions & 11 deletions Cargo.lock

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

22 changes: 21 additions & 1 deletion crates/cheatcodes/assets/cheatcodes.json

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

3 changes: 3 additions & 0 deletions crates/cheatcodes/spec/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
97 changes: 95 additions & 2 deletions crates/cheatcodes/src/utils.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -313,3 +318,91 @@ fn random_int(state: &mut Cheatcodes, bits: Option<U256>) -> 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<String, String> {
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<usize, String> {
// 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)
}
2 changes: 1 addition & 1 deletion docs/dev/cheatcodes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions testdata/cheats/Vm.sol

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

58 changes: 58 additions & 0 deletions testdata/default/cheats/EIP712Hash.t.sol
Original file line number Diff line number Diff line change
@@ -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)"
);
}
}