Skip to content

feat(forge): cheatcode eip712 #10570

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 24 commits into from
May 29, 2025
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
70827ae
wip: eip712 cheacode
0xrusowsky May 19, 2025
ef3c1b9
wip: integrate json-bind
0xrusowsky May 20, 2025
e69ddd7
Merge branch 'master' of github.com:foundry-rs/foundry into chore/che…
0xrusowsky May 20, 2025
d1e1512
finish cheatcode impl + unit tests
0xrusowsky May 20, 2025
9ea76dc
fix: merge conflicts
0xrusowsky May 20, 2025
cf67c2c
style: clippy + fmt
0xrusowsky May 20, 2025
88fb7be
fix: simple example + bump alloy core + docs
0xrusowsky May 20, 2025
a6cca9a
fix: cheats spec
0xrusowsky May 20, 2025
5bcccbc
Merge branch 'feat/eip712-with-solar' into chore/cheat-eip712
grandizzy May 22, 2025
4395b32
load json bindings from foundry.toml + allow custom path + extend tests
0xrusowsky May 22, 2025
cbabab3
style: fmt
0xrusowsky May 22, 2025
7f64f72
fix: test assertion for win
0xrusowsky May 22, 2025
7ff2758
test: custom path
0xrusowsky May 22, 2025
c057902
fix: custom path without being the configured one in `foundry.toml`
0xrusowsky May 22, 2025
c6f037d
fix: use calldata instead of memory in cheatcode fns
0xrusowsky May 22, 2025
4b23502
chore(forge): cheat eip712 struct hash (#10626)
0xrusowsky May 29, 2025
876b7ba
new cheat: eip712 hash typed data
0xrusowsky May 29, 2025
f8dddb6
bump solar and compilers
0xrusowsky May 29, 2025
2c07808
fix: failing compiler test
0xrusowsky May 29, 2025
cac75c3
bump compiler and explorers + patch vyper prague -> cancun
0xrusowsky May 29, 2025
b998d0e
style: clippy
0xrusowsky May 29, 2025
452cbce
fix: check for `.vyi` paths
0xrusowsky May 29, 2025
ce7948f
style: simplify
0xrusowsky May 29, 2025
d35f498
style: simplify
0xrusowsky May 29, 2025
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
352 changes: 175 additions & 177 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ foundry-wallets = { path = "crates/wallets" }
foundry-linking = { path = "crates/linking" }

# solc & compilation utilities

foundry-block-explorers = { version = "0.17.0", default-features = false }
foundry-compilers = { version = "0.16.1", default-features = false }
foundry-fork-db = "0.14"
Expand All @@ -204,7 +205,6 @@ solar-parse = { version = "=0.1.3", default-features = false }
solar-sema = { version = "=0.1.3", default-features = false }
solar-interface = { version = "=0.1.3", default-features = false }


## alloy
alloy-consensus = { version = "1.0.5", default-features = false }
alloy-contract = { version = "1.0.5", default-features = false }
Expand All @@ -229,7 +229,7 @@ alloy-transport-ipc = { version = "1.0.5", default-features = false }
alloy-transport-ws = { version = "1.0.5", default-features = false }

## alloy-core
alloy-dyn-abi = "1.0"
alloy-dyn-abi = "1.1.2"
alloy-json-abi = "1.0"
alloy-primitives = { version = "1.0", features = [
"getrandom",
Expand Down
20 changes: 20 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.

4 changes: 4 additions & 0 deletions crates/cheatcodes/spec/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2888,6 +2888,10 @@ interface Vm {
/// catch (bytes memory interceptedInitcode) { initcode = interceptedInitcode; }
#[cheatcode(group = Utilities, safety = Unsafe)]
function interceptInitcode() external;

/// Randomly shuffles an array.
#[cheatcode(group = Utilities)]
function eip712HashType(string memory typeDefinition) external pure returns (bytes32 typeHash);
}
}

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

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::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_common::{ens::namehash, fs};
use foundry_config::fs_permissions::FsAccessKind;
use foundry_evm_core::constants::DEFAULT_CREATE2_DEPLOYER;
use proptest::prelude::Strategy;
use rand::{seq::SliceRandom, Rng, RngCore};
Expand Down Expand Up @@ -314,3 +315,52 @@ 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 type_def = if !typeDefinition.contains('(') {
get_type_def_from_bindings(typeDefinition, state)?
} else {
EncodeType::parse(typeDefinition).and_then(|parsed| parsed.canonicalize())?
};

Ok(keccak256(type_def.as_bytes()).to_vec())
}
}

fn get_type_def_from_bindings(name: &String, state: &mut Cheatcodes) -> Result<String> {
const TYPE_BINDING_PREFIX: &str = "string constant schema_";

let path = state.config.root.join("utils").join("JsonBindings.sol");
let path = state.config.ensure_path_allowed(&path, FsAccessKind::Read)?;
let content = fs::read_to_string(&path)?;

let type_defs: HashMap<&str, &str> = content
.lines()
.filter_map(|line| {
let relevant = line.trim().strip_prefix(TYPE_BINDING_PREFIX)?;
let (name, def) = relevant.split_once('=')?;
Some((name.trim(), def.trim().strip_prefix('"')?.strip_suffix("\";")?))
})
.collect();

match type_defs.get(name.as_str()) {
Some(value) => Ok(value.to_string()),
None => {
let bindings =
type_defs.keys().map(|k| format!(" - {k}")).collect::<Vec<String>>().join("\n");

bail!(
"'{}' not found in 'utils/JsonBindings.sol'.{}",
name,
if bindings.is_empty() {
String::new()
} else {
format!("\nAvailable bindings:\n{bindings}\n")
}
);
}
}
}
8 changes: 0 additions & 8 deletions crates/forge/tests/cli/eip712.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,31 @@ library Structs {
struct Foo {
Bar bar;
}

struct Bar {
Art art;
}

struct Art {
uint256 id;
}

struct Complex {
Structs2.Foo foo2;
Foo[] foos;
Rec[][] recs;
}

struct Rec {
Rec[] rec;
}
}

library Structs2 {
struct Foo {
uint256 id;
}

struct Rec {
Bar[] bar;
}

struct Bar {
Rec rec;
}

struct FooBar {
Foo[] foos;
Bar[] bars;
Expand Down
73 changes: 73 additions & 0 deletions crates/forge/tests/cli/test_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use alloy_primitives::U256;
use anvil::{spawn, NodeConfig};
use foundry_config::fs_permissions::PathPermission;
use foundry_test_utils::{
rpc, str,
util::{OutputExt, OTHER_SOLC_VERSION, SOLC_VERSION},
Expand Down Expand Up @@ -3649,3 +3650,75 @@ Encountered a total of 1 failing tests, 0 tests succeeded

"#]]);
});

forgetest!(test_eip712_cheatcode, |prj, cmd| {
prj.add_source(
"Eip712",
r#"
contract Eip712Structs {
struct Transaction {
Person from;
Person to;
Asset tx;
}
struct Person {
address wallet;
string name;
}
struct Asset {
address token;
uint256 amount;
}
}
"#,
)
.unwrap();
prj.insert_ds_test();
prj.insert_vm();
prj.insert_console();

prj.add_source("Eip712Cheat.sol", r#"
// Note Used in forge-cli tests to assert failures.
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;

import "./test.sol";
import "./Vm.sol";
import "./console.sol";

string constant CANONICAL = "Transaction(Person from,Person to,Asset tx)Asset(address token,uint256 amount)Person(address wallet,string name)";

contract Eip712Test is DSTest {
Vm constant vm = Vm(HEVM_ADDRESS);

function testEip712HashType() public {
bytes32 canonicalHash = keccak256(bytes(CANONICAL));

// Can figure out the canonical type from a messy string representation of the type,
// with an invalid order and extra whitespaces
bytes32 fromTypeDef = vm.eip712HashType(
"Person(address wallet, string name) Asset(address token, uint256 amount) Transaction(Person from, Person to, Asset tx)"
);
assertEq(fromTypeDef, canonicalHash);

// Can figure out the canonical type from the previously generated bindings
bytes32 fromTypeName = vm.eip712HashType("Transaction");
assertEq(fromTypeName, canonicalHash);

// Reverts if the input type is not found in the bindings
vm._expectCheatcodeRevert();
fromTypeName = vm.eip712HashType("InvalidTypeName");
}
}
"#,
)
.unwrap();

cmd.forge_fuse().args(["bind-json"]).assert_success();

let bindings = prj.root().join("utils").join("JsonBindings.sol");
assert!(bindings.exists(), "'JsonBindings.sol' was not generated at {bindings:?}");

prj.update_config(|config| config.fs_permissions.add(PathPermission::read(bindings)));
cmd.forge_fuse().args(["test", "--mc", "Eip712Test", "-vvvv"]).assert_success();
});
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.