Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
Comment thread
0xrusowsky marked this conversation as resolved.

Large diffs are not rendered by default.

13 changes: 6 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,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,17 +228,17 @@ 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-json-abi = "1.0"
alloy-primitives = { version = "1.0", features = [
alloy-dyn-abi = "1.1.2"
alloy-json-abi = "1.1.2"
alloy-primitives = { version = "1.1.2", features = [
"getrandom",
"rand",
"map-fxhash",
"map-foldhash",
] }
alloy-sol-macro-expander = "1.0"
alloy-sol-macro-input = "1.0"
alloy-sol-types = "1.0"
alloy-sol-macro-expander = "1.1.2"
alloy-sol-macro-input = "1.1.2"
alloy-sol-types = "1.1.2"

alloy-chains = "0.2"
alloy-rlp = "0.3"
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.

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

/// Generates the hash of the canonical EIP-712 type representation.
///
/// Supports 2 different inputs:
/// * Name of the type (i.e. "Transaction") --> requires previous binding generation with `forge bind-json`
/// * String representation of the type (i.e. "Foo(Bar bar) Bar(uint256 baz)") --> the cheatcode
/// will output the canonical type even if the input is malformated (wrong order or whitespaces)
#[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_";
Comment thread
grandizzy marked this conversation as resolved.
Outdated

let path = state.config.root.join("utils").join("JsonBindings.sol");
Comment thread
0xrusowsky marked this conversation as resolved.
Outdated
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;
}

Comment thread
grandizzy marked this conversation as resolved.
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
142 changes: 142 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,144 @@ Encountered a total of 1 failing tests, 0 tests succeeded

"#]]);
});

forgetest!(test_eip712_cheatcode_simple, |prj, cmd| {
prj.add_source(
"Eip712",
r#"
contract Eip712Structs {
struct EIP712Domain {
string name;
string version;
uint256 chainId;
address verifyingContract;
}
}
"#,
)
.unwrap();
prj.insert_ds_test();
prj.insert_vm();
prj.insert_console();

prj.add_source("Eip712Cheat.sol", r#"
import "./test.sol";
import "./Vm.sol";
import "./console.sol";

string constant CANONICAL = "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)";

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

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

// 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(
Comment thread
0xrusowsky marked this conversation as resolved.
"EIP712Domain(string name, string version, uint256 chainId, address verifyingContract)"
);
assertEq(fromTypeDef, canonicalHash);

// Can figure out the canonical type from the previously generated bindings
bytes32 fromTypeName = vm.eip712HashType("EIP712Domain");
assertEq(fromTypeName, canonicalHash);
}
}
"#,
)
.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", "-vv"]).assert_success().stdout_eq(str![
[r#"
[COMPILING_FILES] with [SOLC_VERSION]
[SOLC_VERSION] [ELAPSED]
Compiler run successful!

Ran 1 test for src/Eip712Cheat.sol:Eip712Test
[PASS] testEip712HashType() ([GAS])
Logs:
0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f

Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED]

Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests)

"#]
]);
});

forgetest!(test_eip712_cheatcode_nested, |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#"
import "./test.sol";
import "./Vm.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,
Comment thread
0xrusowsky marked this conversation as resolved.
// 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");
Comment thread
0xrusowsky marked this conversation as resolved.
Outdated
}
}
"#,
)
.unwrap();

cmd.forge_fuse().args(["bind-json"]).assert_success();
Comment thread
grandizzy marked this conversation as resolved.
Outdated

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.