diff --git a/crates/cheatcodes/Cargo.toml b/crates/cheatcodes/Cargo.toml index 8269364d231fb..77cb77ea9beaf 100644 --- a/crates/cheatcodes/Cargo.toml +++ b/crates/cheatcodes/Cargo.toml @@ -65,3 +65,4 @@ tracing.workspace = true walkdir.workspace = true proptest.workspace = true serde.workspace = true + diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json index eb541e269a888..4f8793a2b7bb5 100644 --- a/crates/cheatcodes/assets/cheatcodes.json +++ b/crates/cheatcodes/assets/cheatcodes.json @@ -4294,6 +4294,26 @@ "status": "stable", "safety": "unsafe" }, + { + "func": { + "id": "eip712HashTypedData", + "description": "Generates a ready-to-sign digest of human-readable typed data following the EIP-712 standard.", + "declaration": "function eip712HashTypedData(string calldata jsonData) external pure returns (bytes32 digest);", + "visibility": "external", + "mutability": "pure", + "signature": "eip712HashTypedData(string)", + "selector": "0xea25e615", + "selectorBytes": [ + 234, + 37, + 230, + 21 + ] + }, + "group": "utilities", + "status": "stable", + "safety": "safe" + }, { "func": { "id": "ensNamehash", @@ -9820,6 +9840,26 @@ "status": "stable", "safety": "safe" }, + { + "func": { + "id": "signTypedData", + "description": "Signs human-readable typed data `jsonData` with `privateKey` using the secp256k1 curve.\nThe typed data must follow the EIP-712 standard.", + "declaration": "function signTypedData(string calldata jsonData, uint256 privateKey) external pure returns (uint8 v, bytes32 r, bytes32 s);", + "visibility": "external", + "mutability": "pure", + "signature": "signTypedData(string,uint256)", + "selector": "0x85f00b0d", + "selectorBytes": [ + 133, + 240, + 11, + 13 + ] + }, + "group": "crypto", + "status": "stable", + "safety": "safe" + }, { "func": { "id": "sign_0", diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index 1dac51fd0ca5a..73fb88ad19137 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -2667,6 +2667,12 @@ interface Vm { #[cheatcode(group = Crypto)] function sign(uint256 privateKey, bytes32 digest) external pure returns (uint8 v, bytes32 r, bytes32 s); + /// Signs human-readable typed data `jsonData` with `privateKey` using the secp256k1 curve. + /// + /// The typed data must follow the EIP-712 standard. + #[cheatcode(group = Crypto)] + function signTypedData(string calldata jsonData, uint256 privateKey) external pure returns (uint8 v, bytes32 r,bytes32 s); + /// Signs `digest` with `privateKey` using the secp256k1 curve. /// /// Returns a compact signature (`r`, `vs`) as per EIP-2098, where `vs` encodes both the @@ -2888,6 +2894,10 @@ interface Vm { /// catch (bytes memory interceptedInitcode) { initcode = interceptedInitcode; } #[cheatcode(group = Utilities, safety = Unsafe)] function interceptInitcode() external; + + /// Generates a ready-to-sign digest of human-readable typed data following the EIP-712 standard. + #[cheatcode(group = Utilities)] + function eip712HashTypedData(string calldata jsonData) external pure returns (bytes32 digest); } } diff --git a/crates/cheatcodes/src/crypto.rs b/crates/cheatcodes/src/crypto.rs index be6829ee3110b..adfbf3bbe5a67 100644 --- a/crates/cheatcodes/src/crypto.rs +++ b/crates/cheatcodes/src/crypto.rs @@ -1,6 +1,7 @@ //! Implementations of [`Crypto`](spec::Group::Crypto) Cheatcodes. use crate::{Cheatcode, Cheatcodes, Result, Vm::*}; +use alloy_dyn_abi::eip712::TypedData; use alloy_primitives::{keccak256, Address, B256, U256}; use alloy_signer::{Signer, SignerSync}; use alloy_signer_local::{ @@ -51,6 +52,16 @@ impl Cheatcode for sign_0Call { } } +impl Cheatcode for signTypedDataCall { + fn apply(&self, _state: &mut Cheatcodes) -> Result { + let Self { jsonData, privateKey } = self; + let typed_data: TypedData = serde_json::from_str(jsonData)?; + let digest = typed_data.eip712_signing_hash()?; + let sig = sign(privateKey, &digest)?; + Ok(encode_full_sig(sig)) + } +} + impl Cheatcode for signCompact_0Call { fn apply(&self, _state: &mut Cheatcodes) -> Result { let Self { wallet, digest } = self; diff --git a/crates/cheatcodes/src/utils.rs b/crates/cheatcodes/src/utils.rs index 4bdc239036d53..f61369f40e46c 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::{DynSolType, DynSolValue}; +use alloy_dyn_abi::{DynSolType, DynSolValue, TypedData}; use alloy_ens::namehash; use alloy_primitives::{aliases::B32, map::HashMap, B64, U256}; use alloy_sol_types::SolValue; @@ -314,3 +314,13 @@ fn random_int(state: &mut Cheatcodes, bits: Option) -> Result { .current() .abi_encode()) } + +impl Cheatcode for eip712HashTypedDataCall { + fn apply(&self, _state: &mut Cheatcodes) -> Result { + let Self { jsonData } = self; + let typed_data: TypedData = serde_json::from_str(jsonData)?; + let digest = typed_data.eip712_signing_hash()?; + + Ok(digest.to_vec()) + } +} diff --git a/crates/evm/traces/src/decoder/mod.rs b/crates/evm/traces/src/decoder/mod.rs index 44b9c9cd729b6..776c584e82cd7 100644 --- a/crates/evm/traces/src/decoder/mod.rs +++ b/crates/evm/traces/src/decoder/mod.rs @@ -503,11 +503,12 @@ impl CallTraceDecoder { Some(decoded.iter().map(format_token).collect()) } - "signDelegation" | "signAndAttachDelegation" => { + "signDelegation" | "signAndAttachDelegation" | "signTypedData" => { let mut decoded = func.abi_decode_input(&data[SELECTOR_LEN..]).ok()?; // Redact private key and replace in trace for // signAndAttachDelegation(address implementation, uint256 privateKey) // signDelegation(address implementation, uint256 privateKey) + // signTypedData(string jsonData, uint256 privateKey) decoded[1] = DynSolValue::String("".to_string()); Some(decoded.iter().map(format_token).collect()) } diff --git a/crates/forge/tests/cli/test_cmd.rs b/crates/forge/tests/cli/test_cmd.rs index af4b2aed7d0e7..92fe900b53961 100644 --- a/crates/forge/tests/cli/test_cmd.rs +++ b/crates/forge/tests/cli/test_cmd.rs @@ -3828,3 +3828,52 @@ Encountered a total of 1 failing tests, 0 tests succeeded "#]]); }); + +forgetest!(test_eip712_hash_typed_data, |prj, cmd| { + prj.insert_ds_test(); + prj.insert_vm(); + prj.insert_console(); + + prj.add_source( + "Eip712HashTypedData.sol", + r#" +import "./Vm.sol"; +import "./test.sol"; +import "./console.sol"; + +contract Eip712HashTypedDataTest is DSTest { + Vm constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + + string jsonData = '{"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"},{"name":"salt","type":"bytes32"}]},"primaryType":"EIP712Domain","domain":{"name":"example.metamask.io","version":"1","chainId":1,"verifyingContract":"0x0000000000000000000000000000000000000000"},"message":{}}'; + + function testHashEIP712Message() public { + console.log(jsonData); + + bytes32 cheatMsgHash = vm.eip712HashTypedData(jsonData); + console.log("EIP712Domain message hash from cheatcode:"); + console.logBytes32(cheatMsgHash); + + // since this cheatcode simply exposes an alloy fn, the test has been borrowed from: + // + bytes32 expectedHash = hex"122d1c8ef94b76dad44dcb03fa772361e20855c63311a15d5afe02d1b38f6077"; + assertEq(cheatMsgHash, expectedHash, "EIP712Domain struct hash mismatch"); + } + + function testSignTypedData(uint248 pk) public { + vm.assume(pk != 0); + + (uint8 v, bytes32 r, bytes32 s) = vm.signTypedData(jsonData, pk); + + address expected = vm.addr(pk); + bytes32 digest = vm.eip712HashTypedData(jsonData); + + address actual = ecrecover(digest, v, r, s); + assertEq(actual, expected, "digest signer did not match"); + } +} +"#, + ) + .unwrap(); + + cmd.forge_fuse().args(["test", "--mc", "Eip712HashTypedDataTest", "-vvvv"]).assert_success(); +}); diff --git a/testdata/cheats/Vm.sol b/testdata/cheats/Vm.sol index 6d054abbfc6ec..cdcf561d56de7 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 eip712HashTypedData(string calldata jsonData) external pure returns (bytes32 digest); 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); @@ -484,6 +485,7 @@ interface Vm { function signDelegation(address implementation, uint256 privateKey, uint64 nonce) external returns (SignedDelegation memory signedDelegation); function signDelegation(address implementation, uint256 privateKey, bool crossChain) external returns (SignedDelegation memory signedDelegation); function signP256(uint256 privateKey, bytes32 digest) external pure returns (bytes32 r, bytes32 s); + function signTypedData(string calldata jsonData, uint256 privateKey) external pure returns (uint8 v, bytes32 r, bytes32 s); function sign(Wallet calldata wallet, bytes32 digest) external returns (uint8 v, bytes32 r, bytes32 s); function sign(uint256 privateKey, bytes32 digest) external pure returns (uint8 v, bytes32 r, bytes32 s); function sign(bytes32 digest) external pure returns (uint8 v, bytes32 r, bytes32 s);