From e648915b7fafb1e09e47975b14a6725e0470fb28 Mon Sep 17 00:00:00 2001 From: Blaine Malone Date: Tue, 24 Jun 2025 15:51:02 -0400 Subject: [PATCH 1/6] feat(eip712): Migrate to cast wallet sign for sigining eip712 data --- src/libraries/EIP712.sol | 25 +++++++++++ src/libraries/GnosisSafeHashes.sol | 68 ++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 src/libraries/EIP712.sol diff --git a/src/libraries/EIP712.sol b/src/libraries/EIP712.sol new file mode 100644 index 0000000000..ff57878a53 --- /dev/null +++ b/src/libraries/EIP712.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.15; + +import "forge-std/Script.sol"; +import {GnosisSafeHashes} from "src/libraries/GnosisSafeHashes.sol"; + +contract EIP712 is Script { + function run() public { + bytes memory data = + hex"1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"; + GnosisSafeHashes.SafeTransaction memory safeTx = GnosisSafeHashes.SafeTransaction({ + to: 0x1111111111111111111111111111111111111111, + value: 0, + data: data, + operation: 0, + safeTxGas: 0, + baseGas: 0, + gasPrice: 0, + gasToken: 0x0000000000000000000000000000000000000000, + refundReceiver: 0x0000000000000000000000000000000000000000, + nonce: 1 + }); + GnosisSafeHashes.generateTypedDataJson(100, 0x2222222222222222222222222222222222222222, safeTx); + } +} diff --git a/src/libraries/GnosisSafeHashes.sol b/src/libraries/GnosisSafeHashes.sol index ae4c414088..0869c98c0d 100644 --- a/src/libraries/GnosisSafeHashes.sol +++ b/src/libraries/GnosisSafeHashes.sol @@ -7,10 +7,15 @@ import {GnosisSafe} from "lib/safe-contracts/contracts/GnosisSafe.sol"; import {IMulticall3} from "forge-std/interfaces/IMulticall3.sol"; import {VmSafe} from "forge-std/Vm.sol"; import {IGnosisSafe, Enum} from "@base-contracts/script/universal/IGnosisSafe.sol"; +import {Vm} from "forge-std/Vm.sol"; +import {console} from "forge-std/console.sol"; /// @title GnosisSafeHashes /// @notice Library for calculating domain separators and message hashes for Gnosis Safe transactions library GnosisSafeHashes { + address internal constant VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); + Vm internal constant vm = Vm(VM_ADDRESS); + // Safe transaction type hash bytes32 constant SAFE_TX_TYPEHASH = keccak256( "SafeTx(address to,uint256 value,bytes data,uint8 operation,uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,address gasToken,address refundReceiver,uint256 nonce)" @@ -274,4 +279,67 @@ library GnosisSafeHashes { ) ); } + + /// @notice Struct for a Safe transaction. Used as the EIP-712 hash struct. + struct SafeTransaction { + address to; + uint256 value; + bytes data; + uint8 operation; + uint256 safeTxGas; + uint256 baseGas; + uint256 gasPrice; + address gasToken; + address refundReceiver; + uint256 nonce; + } + + /// @notice Generates a JSON string for the EIP-712 typed data. This string that is logged must be passed as an arg + /// to cast e.g. 'cast wallet sign --ledger --data "$(forge script EIP712 --json | jq -r '.logs[0]')"'. + function generateTypedDataJson(uint256 chainId, address verifyingContract, SafeTransaction memory safeTx) + external + pure + { + console.log( + string.concat( + "{\n", + ' "types": {\n', + ' "EIP712Domain": [\n', + ' { "name": "chainId", "type": "uint256" }, \n', + ' { "name": "verifyingContract", "type": "address" }\n', + " ],\n", + ' "SafeTx": [\n', + ' { "name": "to", "type": "address" },\n', + ' { "name": "value", "type": "uint256" },\n', + ' { "name": "data", "type": "bytes" },\n', + ' { "name": "operation", "type": "uint8" },\n', + ' { "name": "safeTxGas", "type": "uint256" },\n', + ' { "name": "baseGas", "type": "uint256" },\n', + ' { "name": "gasPrice", "type": "uint256" },\n', + ' { "name": "gasToken", "type": "address" },\n', + ' { "name": "refundReceiver", "type": "address" },\n', + ' { "name": "nonce", "type": "uint256" }\n', + " ]\n", + " },\n", + ' "primaryType": "SafeTx",\n', + ' "domain": {\n', + string.concat(' "chainId": ', vm.toString(chainId), ",\n"), + string.concat(' "verifyingContract": "', vm.toString(verifyingContract), '"\n'), + " },\n", + ' "message": {\n', + string.concat(' "to": "', vm.toString(safeTx.to), '",\n'), + string.concat(' "value": ', vm.toString(safeTx.value), ",\n"), + string.concat(' "data": "', vm.toString(safeTx.data), '",\n'), + string.concat(' "operation": ', vm.toString(uint256(safeTx.operation)), ",\n"), + string.concat(' "safeTxGas": ', vm.toString(safeTx.safeTxGas), ",\n"), + string.concat(' "baseGas": ', vm.toString(safeTx.baseGas), ",\n"), + string.concat(' "gasPrice": ', vm.toString(safeTx.gasPrice), ",\n"), + string.concat(' "gasToken": "', vm.toString(safeTx.gasToken), '",\n'), + string.concat(' "refundReceiver": "', vm.toString(safeTx.refundReceiver), '",\n'), + string.concat(' "nonce": ', vm.toString(safeTx.nonce), "\n"), + " }\n", + "}\n" + ) + ); + } } From ae799b6a32ac3275cb0ca8e2fd2285e0e37db1c4 Mon Sep 17 00:00:00 2001 From: Blaine Malone Date: Tue, 24 Jun 2025 16:07:57 -0400 Subject: [PATCH 2/6] fix: nit linting --- src/libraries/EIP712.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/EIP712.sol b/src/libraries/EIP712.sol index ff57878a53..1d8e3cc9f0 100644 --- a/src/libraries/EIP712.sol +++ b/src/libraries/EIP712.sol @@ -5,7 +5,7 @@ import "forge-std/Script.sol"; import {GnosisSafeHashes} from "src/libraries/GnosisSafeHashes.sol"; contract EIP712 is Script { - function run() public { + function run() public pure { bytes memory data = hex"1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"; GnosisSafeHashes.SafeTransaction memory safeTx = GnosisSafeHashes.SafeTransaction({ From 02f7b86e930895de7dae50db3f0fe2c54cf3aaef Mon Sep 17 00:00:00 2001 From: blaine Date: Mon, 29 Sep 2025 14:27:51 -0400 Subject: [PATCH 3/6] fix: EIP712 encoding --- README.md | 2 + src/libraries/EIP712.sol | 25 ---- src/libraries/GnosisSafeHashes.sol | 163 ++++++++++++++++---------- src/libraries/MultisigTaskPrinter.sol | 6 +- src/libraries/Utils.sol | 8 ++ src/tasks/MultisigTask.sol | 46 +++++++- src/tasks/TaskManager.sol | 2 +- test/libraries/GnosisSafeHashes.t.sol | 143 +++++++++++++++++++--- 8 files changed, 281 insertions(+), 114 deletions(-) delete mode 100644 src/libraries/EIP712.sol diff --git a/README.md b/README.md index 616c00444f..d33bf0760d 100644 --- a/README.md +++ b/README.md @@ -199,6 +199,8 @@ just sign-stack [child-safe-name-depth-1] [child-safe-name-dept - `USE_KEYSTORE` - If set, uses keystore instead of ledger. By default, keys are stored under `~/.foundry/keystores`. - `SIMULATE_WITHOUT_WALLET` - Set to run the simulation phase without a wallet (i.e. Ledger/Trezor/KeyStore). This step will simulate a task with valid, default keys. - `WALLET_TYPE` - This can be either `ledger`, `trezor` or `keystore`. The default value is `ledger`. +- `USE_KEYSTORE` - If set, uses keystore instead of ledger. By default, keys are stored under `~/.foundry/keystores`. +- `EIP712_ENCODED_DATA_TO_SIGN` - If set the encoded EIP-712 typed data JSON is used for signing; when unset, only the EIP-712 domain and message hashes (default) are used for signing. **Examples:** diff --git a/src/libraries/EIP712.sol b/src/libraries/EIP712.sol deleted file mode 100644 index 1d8e3cc9f0..0000000000 --- a/src/libraries/EIP712.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.15; - -import "forge-std/Script.sol"; -import {GnosisSafeHashes} from "src/libraries/GnosisSafeHashes.sol"; - -contract EIP712 is Script { - function run() public pure { - bytes memory data = - hex"1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"; - GnosisSafeHashes.SafeTransaction memory safeTx = GnosisSafeHashes.SafeTransaction({ - to: 0x1111111111111111111111111111111111111111, - value: 0, - data: data, - operation: 0, - safeTxGas: 0, - baseGas: 0, - gasPrice: 0, - gasToken: 0x0000000000000000000000000000000000000000, - refundReceiver: 0x0000000000000000000000000000000000000000, - nonce: 1 - }); - GnosisSafeHashes.generateTypedDataJson(100, 0x2222222222222222222222222222222222222222, safeTx); - } -} diff --git a/src/libraries/GnosisSafeHashes.sol b/src/libraries/GnosisSafeHashes.sol index 0869c98c0d..539724398b 100644 --- a/src/libraries/GnosisSafeHashes.sol +++ b/src/libraries/GnosisSafeHashes.sol @@ -9,6 +9,7 @@ import {VmSafe} from "forge-std/Vm.sol"; import {IGnosisSafe, Enum} from "@base-contracts/script/universal/IGnosisSafe.sol"; import {Vm} from "forge-std/Vm.sol"; import {console} from "forge-std/console.sol"; +import {stdJson} from "forge-std/StdJson.sol"; /// @title GnosisSafeHashes /// @notice Library for calculating domain separators and message hashes for Gnosis Safe transactions @@ -169,27 +170,78 @@ library GnosisSafeHashes { } /// @notice Reads the result of a call to Safe.encodeTransactionData and returns the message hash. - function getDomainAndMessageHashFromEncodedTransactionData(bytes memory _encodedTxData) + function getDomainAndMessageHashFromDataToSign(bytes memory _dataToSign) internal pure returns (bytes32 domainSeparator_, bytes32 messageHash_) { - require(_encodedTxData.length == 66, "GnosisSafeHashes: Invalid encoded transaction data length."); - require(_encodedTxData[0] == bytes1(0x19), "GnosisSafeHashes: Expected prefix byte 0x19."); - require(_encodedTxData[1] == bytes1(0x01), "GnosisSafeHashes: Expected prefix byte 0x01."); - - // Memory layout of a `bytes` array in Solidity: - // - The first 32 bytes store the array length (66 bytes here). - // - The actual data starts immediately after the length. - // Our data structure is: - // [0x19][0x01][32-byte domainSeparator][32-byte messageHash] - // The message hash begins at offset: 32 (skip length) + 34 = 66. - assembly { - // Domain separator starts after 2-byte prefix (offset 34 in bytes array) - domainSeparator_ := mload(add(_encodedTxData, 34)) - // Message hash starts at offset 66 (after domain separator) - messageHash_ := mload(add(_encodedTxData, 66)) + // If it looks like 0x1901-prefixed encoded bytes (66 bytes total), decode directly. + if (_dataToSign.length == 66 && _dataToSign[0] == bytes1(0x19) && _dataToSign[1] == bytes1(0x01)) { + // Memory layout of a `bytes` array in Solidity: + // - The first 32 bytes store the array length (66 bytes here). + // - The actual data starts immediately after the length. + // Our data structure is: + // [0x19][0x01][32-byte domainSeparator][32-byte messageHash] + // The message hash begins at offset: 32 (skip length) + 34 = 66. + assembly { + domainSeparator_ := mload(add(_dataToSign, 34)) + messageHash_ := mload(add(_dataToSign, 66)) + } + return (domainSeparator_, messageHash_); } + // Otherwise, assume EIP-712 JSON produced by encodeEIP712Json and compute from JSON. + return getDomainAndMessageHashFromEip712Json(_dataToSign); + } + + /// @notice Computes domain separator and message hash from an EIP-712 JSON payload produced by encodeEIP712Json. + /// @dev This mirrors the Safe EIP-712 encoding: dynamic types such as bytes are hashed before struct encoding. + function getDomainAndMessageHashFromEip712Json(bytes memory _json) + internal + pure + returns (bytes32 domainSeparator_, bytes32 messageHash_) + { + string memory json = string(_json); + require(bytes(json).length != 0, "GnosisSafeHashes: empty EIP-712 JSON"); + + // Domain fields + uint256 chainId = stdJson.readUint(json, ".domain.chainId"); + address verifyingContract = stdJson.readAddress(json, ".domain.verifyingContract"); + require(verifyingContract != address(0), "GnosisSafeHashes: verifyingContract is zero"); + // Compute domain separator according to the JSON schema used in encodeEIP712Json + domainSeparator_ = keccak256( + abi.encode(keccak256("EIP712Domain(uint256 chainId,address verifyingContract)"), chainId, verifyingContract) + ); + + // Message fields + address to = stdJson.readAddress(json, ".message.to"); + require(to != address(0), "GnosisSafeHashes: to is zero"); + uint256 value = stdJson.readUint(json, ".message.value"); + bytes memory data = stdJson.readBytes(json, ".message.data"); + uint8 operation = uint8(stdJson.readUint(json, ".message.operation")); + require(operation == 1, "GnosisSafeHashes: invalid operation, only DelegateCall is supported"); + uint256 safeTxGas = stdJson.readUint(json, ".message.safeTxGas"); + uint256 baseGas = stdJson.readUint(json, ".message.baseGas"); + uint256 gasPrice = stdJson.readUint(json, ".message.gasPrice"); + address gasToken = stdJson.readAddress(json, ".message.gasToken"); + address refundReceiver = stdJson.readAddress(json, ".message.refundReceiver"); + uint256 nonce = stdJson.readUint(json, ".message.nonce"); + + bytes32 dataHash = keccak256(data); + messageHash_ = keccak256( + abi.encode( + SAFE_TX_TYPEHASH, + to, + value, + dataHash, + operation, + safeTxGas, + baseGas, + gasPrice, + gasToken, + refundReceiver, + nonce + ) + ); } /// @notice Helper to decode multicall calldata and extract approveHash parameter. @@ -294,52 +346,39 @@ library GnosisSafeHashes { uint256 nonce; } - /// @notice Generates a JSON string for the EIP-712 typed data. This string that is logged must be passed as an arg - /// to cast e.g. 'cast wallet sign --ledger --data "$(forge script EIP712 --json | jq -r '.logs[0]')"'. - function generateTypedDataJson(uint256 chainId, address verifyingContract, SafeTransaction memory safeTx) - external - pure + /// @notice Encodes the EIP-712 JSON for a Safe transaction. Taken and modified from: + /// https://github.com/base/contracts/pull/148/files#diff-72074ac05b528d57b61e77e3e0c6796a09cd56c4966a3d88a2c086b4a539ffce + function encodeEIP712Json(address _multicallTarget, address _safe, SafeTransaction memory _safeTx) + internal + returns (bytes memory) { - console.log( - string.concat( - "{\n", - ' "types": {\n', - ' "EIP712Domain": [\n', - ' { "name": "chainId", "type": "uint256" }, \n', - ' { "name": "verifyingContract", "type": "address" }\n', - " ],\n", - ' "SafeTx": [\n', - ' { "name": "to", "type": "address" },\n', - ' { "name": "value", "type": "uint256" },\n', - ' { "name": "data", "type": "bytes" },\n', - ' { "name": "operation", "type": "uint8" },\n', - ' { "name": "safeTxGas", "type": "uint256" },\n', - ' { "name": "baseGas", "type": "uint256" },\n', - ' { "name": "gasPrice", "type": "uint256" },\n', - ' { "name": "gasToken", "type": "address" },\n', - ' { "name": "refundReceiver", "type": "address" },\n', - ' { "name": "nonce", "type": "uint256" }\n', - " ]\n", - " },\n", - ' "primaryType": "SafeTx",\n', - ' "domain": {\n', - string.concat(' "chainId": ', vm.toString(chainId), ",\n"), - string.concat(' "verifyingContract": "', vm.toString(verifyingContract), '"\n'), - " },\n", - ' "message": {\n', - string.concat(' "to": "', vm.toString(safeTx.to), '",\n'), - string.concat(' "value": ', vm.toString(safeTx.value), ",\n"), - string.concat(' "data": "', vm.toString(safeTx.data), '",\n'), - string.concat(' "operation": ', vm.toString(uint256(safeTx.operation)), ",\n"), - string.concat(' "safeTxGas": ', vm.toString(safeTx.safeTxGas), ",\n"), - string.concat(' "baseGas": ', vm.toString(safeTx.baseGas), ",\n"), - string.concat(' "gasPrice": ', vm.toString(safeTx.gasPrice), ",\n"), - string.concat(' "gasToken": "', vm.toString(safeTx.gasToken), '",\n'), - string.concat(' "refundReceiver": "', vm.toString(safeTx.refundReceiver), '",\n'), - string.concat(' "nonce": ', vm.toString(safeTx.nonce), "\n"), - " }\n", - "}\n" - ) - ); + string memory types = '{"EIP712Domain":[' '{"name":"chainId","type":"uint256"},' + '{"name":"verifyingContract","type":"address"}],' '"SafeTx":[' '{"name":"to","type":"address"},' + '{"name":"value","type":"uint256"},' '{"name":"data","type":"bytes"},' + '{"name":"operation","type":"uint8"},' '{"name":"safeTxGas","type":"uint256"},' + '{"name":"baseGas","type":"uint256"},' '{"name":"gasPrice","type":"uint256"},' + '{"name":"gasToken","type":"address"},' '{"name":"refundReceiver","type":"address"},' + '{"name":"nonce","type":"uint256"}]}'; + + string memory domain = stdJson.serialize("domain", "chainId", uint256(block.chainid)); + domain = stdJson.serialize("domain", "verifyingContract", address(_safe)); + + string memory message = stdJson.serialize("message", "to", _multicallTarget); + message = stdJson.serialize("message", "value", _safeTx.value); + message = stdJson.serialize("message", "data", _safeTx.data); + message = stdJson.serialize("message", "operation", uint256(_safeTx.operation)); + message = stdJson.serialize("message", "safeTxGas", uint256(_safeTx.safeTxGas)); + message = stdJson.serialize("message", "baseGas", uint256(_safeTx.baseGas)); + message = stdJson.serialize("message", "gasPrice", uint256(_safeTx.gasPrice)); + message = stdJson.serialize("message", "gasToken", address(_safeTx.gasToken)); + message = stdJson.serialize("message", "refundReceiver", address(_safeTx.refundReceiver)); + message = stdJson.serialize("message", "nonce", _safeTx.nonce); + + string memory json = stdJson.serialize("", "primaryType", string("SafeTx")); + json = stdJson.serialize("", "types", types); + json = stdJson.serialize("", "domain", domain); + json = stdJson.serialize("", "message", message); + + return abi.encodePacked(json); } } diff --git a/src/libraries/MultisigTaskPrinter.sol b/src/libraries/MultisigTaskPrinter.sol index 5d999b5b2f..53acd88248 100644 --- a/src/libraries/MultisigTaskPrinter.sol +++ b/src/libraries/MultisigTaskPrinter.sol @@ -66,8 +66,10 @@ library MultisigTaskPrinter { } /// @notice Prints encoded transaction data with formatted header and footer and instructions for signers. - /// @param dataToSign The encoded transaction data to sign. - function printEncodedTransactionData(bytes memory dataToSign) internal view { + /// @param dataToSign The encoded transaction data to sign. This can be encoded either as: + /// 1. EIP-712 Json string + /// 2. 0x1901<32-byte domainSeparator><32-byte messageHash> + function printDataToSign(bytes memory dataToSign) internal view { // NOTE: Do not change the vvvvvvvv and ^^^^^^^^ lines, as the eip712sign tool explicitly // looks for those specific lines to identify the data to sign. printTitle("DATA TO SIGN"); diff --git a/src/libraries/Utils.sol b/src/libraries/Utils.sol index 67ca0337af..8f54345cea 100644 --- a/src/libraries/Utils.sol +++ b/src/libraries/Utils.sol @@ -31,6 +31,14 @@ library Utils { return false; } + /// @notice Controls whether the safe tx is printed as structured EIP-712 data, or just hashes. + /// If you want to print and sign hashed EIP-712 data (domain + message hash) rather than the + /// typed EIP-712 data struct, set the environment variable accordingly. + /// We default to printing the hashes and not the structured data. + function printDataHashes() internal view returns (bool) { + return !isFeatureEnabled("EIP712_ENCODED_DATA_TO_SIGN"); + } + /// @notice Checks that values have code on this chain. /// This method is not storage-layout-aware and therefore is not perfect. It may return erroneous /// results for cases like packed slots, and silently show that things are okay when they are not. diff --git a/src/tasks/MultisigTask.sol b/src/tasks/MultisigTask.sol index 6bcb44d496..f9e0b6c055 100644 --- a/src/tasks/MultisigTask.sol +++ b/src/tasks/MultisigTask.sol @@ -797,10 +797,8 @@ abstract contract MultisigTask is Test, Script, StateOverrideManager, TaskManage MultisigTaskPrinter.printTitle(string.concat("Safe (Depth: ", vm.toString(level), ")")); console.log("Safe Address: ", MultisigTaskPrinter.getAddressLabel(payload.safes[i])); console.log("Safe Hash: ", vm.toString(safeHash)); - address multicallAddress = _getMulticallAddress(payload.safes[i], payload.safes); - dataToSign_ = GnosisSafeHashes.getEncodedTransactionData( - payload.safes[i], payload.calldatas[i], 0, payload.originalNonces[i], multicallAddress - ); + dataToSign_ = + _getDataToSign(payload.calldatas[i], payload.safes[i], payload.originalNonces[i], payload.safes); bool isLastTask = i == 0; if (isLastTask) { @@ -814,13 +812,49 @@ abstract contract MultisigTask is Test, Script, StateOverrideManager, TaskManage MultisigTaskPrinter.printNormalizedStateDiffHash(normalizedHash_); } + /// @notice Builds the data that signers should sign for a given safe and call. + /// If Utils.printDataHashes() is true, returns the encoded transaction data; otherwise returns EIP-712 JSON. + function _getDataToSign(bytes memory callData, address safe, uint256 originalNonce, address[] memory allSafes) + internal + returns (bytes memory dataToSign_) + { + address multicallAddress = _getMulticallAddress(safe, allSafes); + bytes memory dataToSignDataHashes = + GnosisSafeHashes.getEncodedTransactionData(safe, callData, 0, originalNonce, multicallAddress); + bytes memory dataToSignJson = GnosisSafeHashes.encodeEIP712Json( + multicallAddress, + safe, + GnosisSafeHashes.SafeTransaction({ + to: multicallAddress, + value: 0, + data: callData, + operation: uint8(Enum.Operation.DelegateCall), + safeTxGas: 0, + baseGas: 0, + gasPrice: 0, + gasToken: address(0), + refundReceiver: address(0), + nonce: originalNonce + }) + ); + (bytes32 domainHashFromData, bytes32 messageHashFromData) = + GnosisSafeHashes.getDomainAndMessageHashFromDataToSign(dataToSignDataHashes); + (bytes32 domainHashFromJson, bytes32 messageHashFromJson) = + GnosisSafeHashes.getDomainAndMessageHashFromDataToSign(dataToSignJson); + require( + messageHashFromData == messageHashFromJson && domainHashFromData == domainHashFromJson, + "MultisigTask: EIP712 data to sign hashes do not match. Please report this error." + ); + return Utils.printDataHashes() ? dataToSignDataHashes : dataToSignJson; + } + /// @notice Helper function to print the final safe information. function _printLastSafe(bytes memory dataToSign, address rootSafe, TaskPayload memory payload) private view { (bytes32 domainSeparator, bytes32 messageHash) = - GnosisSafeHashes.getDomainAndMessageHashFromEncodedTransactionData(dataToSign); + GnosisSafeHashes.getDomainAndMessageHashFromDataToSign(dataToSign); console.log("Domain Hash: ", vm.toString(domainSeparator)); console.log("Message Hash: ", vm.toString(messageHash)); - MultisigTaskPrinter.printEncodedTransactionData(dataToSign); + MultisigTaskPrinter.printDataToSign(dataToSign); address rootMulticallTarget = _getMulticallAddress(rootSafe, payload.safes); address childMulticallTarget = payload.safes.length > 1 ? _getMulticallAddress(payload.safes[0], payload.safes) : address(0); diff --git a/src/tasks/TaskManager.sol b/src/tasks/TaskManager.sol index c260dab888..a7a8ea9cb4 100644 --- a/src/tasks/TaskManager.sol +++ b/src/tasks/TaskManager.sol @@ -207,7 +207,7 @@ contract TaskManager is Script { function checkDataToSign(bytes memory _dataToSign, TaskConfig memory _config) public view returns (bool) { string memory message = "Please check that you've added it to the VALIDATION markdown file."; (bytes32 domainSeparator, bytes32 messageHash) = - GnosisSafeHashes.getDomainAndMessageHashFromEncodedTransactionData(_dataToSign); + GnosisSafeHashes.getDomainAndMessageHashFromDataToSign(_dataToSign); bytes memory domainHashBytes = abi.encodePacked(domainSeparator); bytes memory messageHashBytes = abi.encodePacked(messageHash); bool containsDomainHash = diff --git a/test/libraries/GnosisSafeHashes.t.sol b/test/libraries/GnosisSafeHashes.t.sol index 144aa69053..81a2f150ed 100644 --- a/test/libraries/GnosisSafeHashes.t.sol +++ b/test/libraries/GnosisSafeHashes.t.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.15; import {IMulticall3} from "forge-std/interfaces/IMulticall3.sol"; import "forge-std/Test.sol"; +import {stdJson} from "forge-std/StdJson.sol"; import {GnosisSafeHashes} from "src/libraries/GnosisSafeHashes.sol"; import {IGnosisSafe, Enum} from "@base-contracts/script/universal/IGnosisSafe.sol"; import {VmSafe} from "forge-std/Vm.sol"; @@ -10,6 +11,46 @@ import {VmSafe} from "forge-std/Vm.sol"; contract GnosisSafeHashes_Test is Test { using GnosisSafeHashes for bytes; + /// @notice Returns the common long `message.data` bytes used in EIP-712 JSON tests. + function _defaultData() internal pure returns (bytes memory) { + return + hex"174dea710000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000006d5b183f538abb8572f5cd17109c617b994d58330000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000024d4d9bdcd2d714b9e5365bc8bfb6929dcaa90a870cc1a2a19b93ac61ea078f5b95253aae900000000000000000000000000000000000000000000000000000000"; + } + + /// @notice Helper to build the EIP-712 JSON payload used by tests. + function _buildEip712Json( + uint256 chainId, + address verifyingContract, + address to, + bytes memory data, + uint8 operation, + uint256 nonce + ) internal returns (bytes memory) { + string memory types = + '{"EIP712Domain":[{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"SafeTx":[{"name":"to","type":"address"},{"name":"value","type":"uint256"},{"name":"data","type":"bytes"},{"name":"operation","type":"uint8"},{"name":"safeTxGas","type":"uint256"},{"name":"baseGas","type":"uint256"},{"name":"gasPrice","type":"uint256"},{"name":"gasToken","type":"address"},{"name":"refundReceiver","type":"address"},{"name":"nonce","type":"uint256"}]}'; + + string memory domain = stdJson.serialize("domain", "chainId", chainId); + domain = stdJson.serialize("domain", "verifyingContract", verifyingContract); + + string memory message = stdJson.serialize("message", "to", to); + message = stdJson.serialize("message", "value", uint256(0)); + message = stdJson.serialize("message", "data", data); + message = stdJson.serialize("message", "operation", uint256(operation)); + message = stdJson.serialize("message", "safeTxGas", uint256(0)); + message = stdJson.serialize("message", "baseGas", uint256(0)); + message = stdJson.serialize("message", "gasPrice", uint256(0)); + message = stdJson.serialize("message", "gasToken", address(0)); + message = stdJson.serialize("message", "refundReceiver", address(0)); + message = stdJson.serialize("message", "nonce", nonce); + + string memory json = stdJson.serialize("", "primaryType", string("SafeTx")); + json = stdJson.serialize("", "types", types); + json = stdJson.serialize("", "domain", domain); + json = stdJson.serialize("", "message", message); + + return abi.encodePacked(json); + } + /// @notice Test calculateMessageHashFromCalldata with valid input function testCalculateMessageHashFromCalldata_ValidInput() public pure { address to = address(0x1234567890123456789012345678901234567890); @@ -106,61 +147,127 @@ contract GnosisSafeHashes_Test is Test { /// @notice Test with valid input. The encoded data is constructed as: /// [0x19, 0x01, 32 bytes domain separator (zeros), 32 bytes message hash]. - function testGetMessageHashFromEncodedTransactionData_ValidInput() public pure { + function testGetDomainAndMessageHashFromDataToSign_ValidInput() public pure { bytes32 expectedDomainSeparator = bytes32(hex"0000000000000000000000000000000000000000000000000000000000001234"); bytes32 expectedMessageHash = bytes32(hex"000000000000000000000000000000000000000000000000000000000000abcd"); bytes memory encodedTxData = abi.encodePacked(bytes2(0x1901), expectedDomainSeparator, expectedMessageHash); - (bytes32 domainSeparator, bytes32 messageHash) = - encodedTxData.getDomainAndMessageHashFromEncodedTransactionData(); + (bytes32 domainSeparator, bytes32 messageHash) = encodedTxData.getDomainAndMessageHashFromDataToSign(); assertEq(domainSeparator, expectedDomainSeparator, "Domain separator should be all zeros"); assertEq(messageHash, expectedMessageHash, "Message hash should match the last 32 bytes"); } /// @notice Test where the message hash is all zeros. - function testGetMessageHashFromEncodedTransactionData_AllZeros() public pure { + function testGetDomainAndMessageHashFromDataToSign_AllZeros() public pure { bytes memory encodedTxData = abi.encodePacked(bytes2(0x1901), bytes32(0), bytes32(0)); bytes32 expectedHash = bytes32(0); - (bytes32 domainSeparator, bytes32 messageHash) = - encodedTxData.getDomainAndMessageHashFromEncodedTransactionData(); + (bytes32 domainSeparator, bytes32 messageHash) = encodedTxData.getDomainAndMessageHashFromDataToSign(); assertEq(domainSeparator, expectedHash, "Domain separator should be all zeros"); assertEq(messageHash, expectedHash, "Message hash should be all zeros"); } - function testGetDomainAndMessageHashFromEncodedTransactionData_MaxUint256() public pure { + function testGetDomainAndMessageHashFromDataToSign_MaxUint256() public pure { bytes memory encodedTxData = abi.encodePacked(bytes2(0x1901), bytes32(type(uint256).max), bytes32(type(uint256).max)); bytes32 expectedHash = bytes32(type(uint256).max); - (bytes32 domainSeparator, bytes32 messageHash) = - encodedTxData.getDomainAndMessageHashFromEncodedTransactionData(); + (bytes32 domainSeparator, bytes32 messageHash) = encodedTxData.getDomainAndMessageHashFromDataToSign(); assertEq(domainSeparator, expectedHash, "Domain separator should be the max uint256"); assertEq(messageHash, expectedHash, "Message hash should be the max uint256"); } /// forge-config: default.allow_internal_expect_revert = true - function testGetMessageHashFromEncodedTransactionData_TooShort() public { + function testGetDomainAndMessageHashFromDataToSign_TooShort() public { // Only 6 bytes (too short) bytes memory encodedTxData = hex"1901deadbeef"; - vm.expectRevert("GnosisSafeHashes: Invalid encoded transaction data length."); - encodedTxData.getDomainAndMessageHashFromEncodedTransactionData(); + vm.expectRevert(); // Tries to parse data as EIP-712 JSON but it reverts. + encodedTxData.getDomainAndMessageHashFromDataToSign(); } /// forge-config: default.allow_internal_expect_revert = true - function testGetMessageHashFromEncodedTransactionData_TooLong() public { - // 67 bytes (too long) + function testGetDomainAndMessageHashFromDataToSign_IncorrectEIP712Json() public { + // 67 bytes that is not a valid EIP-712 JSON. bytes memory encodedTxData = new bytes(67); - vm.expectRevert("GnosisSafeHashes: Invalid encoded transaction data length."); - encodedTxData.getDomainAndMessageHashFromEncodedTransactionData(); + vm.expectRevert(); + encodedTxData.getDomainAndMessageHashFromDataToSign(); } - function testGetMessageHashFromEncodedTransactionData_FuzzTest(bytes32 randomHash) public pure { + function testGetDomainAndMessageHashFromDataToSign_FuzzTest(bytes32 randomHash) public pure { bytes memory encodedTxData = abi.encodePacked(bytes2(0x1901), bytes32(0), randomHash); - (, bytes32 messageHash) = encodedTxData.getDomainAndMessageHashFromEncodedTransactionData(); + (, bytes32 messageHash) = encodedTxData.getDomainAndMessageHashFromDataToSign(); assertEq(messageHash, randomHash, "Message hash should match the input random hash"); } + function testGetDomainAndMessageHashFromEip712Json() public { + bytes memory payload = _buildEip712Json({ + chainId: 1, + verifyingContract: 0x847B5c174615B1B7fDF770882256e2D3E95b9D92, + to: 0xcA11bde05977b3631167028862bE2a173976CA11, + data: _defaultData(), + operation: 1, + nonce: 36 + }); + + // Expected hashes provided by the user for this exact JSON + // These hashes are taken from 'eth/024-U16a-opcm-upgrade-v410-unichain'. + bytes32 expectedDomainHash = bytes32(hex"a4a9c312badf3fcaa05eafe5dc9bee8bd9316c78ee8b0bebe3115bb21b732672"); + bytes32 expectedMessageHash = bytes32(hex"01bdc123c9d8c24f36748875ebbfa43edc5be26a165455e9ea8cb0668c4f9feb"); + + (bytes32 domainHash, bytes32 messageHash) = payload.getDomainAndMessageHashFromDataToSign(); + assertEq(domainHash, expectedDomainHash, "Domain hash should match expected value"); + assertEq(messageHash, expectedMessageHash, "Message hash should match expected value"); + } + + /// forge-config: default.allow_internal_expect_revert = true + function testGetDomainAndMessageHashFromEip712Json_InvalidOperation() public { + bytes memory payload = _buildEip712Json({ + chainId: 1, + verifyingContract: 0x847B5c174615B1B7fDF770882256e2D3E95b9D92, + to: 0xcA11bde05977b3631167028862bE2a173976CA11, + data: _defaultData(), + operation: 0, + nonce: 36 + }); + vm.expectRevert("GnosisSafeHashes: invalid operation, only DelegateCall is supported"); + payload.getDomainAndMessageHashFromDataToSign(); + } + + /// forge-config: default.allow_internal_expect_revert = true + function testGetDomainAndMessageHashFromEip712Json_ZeroVerifyingContract() public { + bytes memory payload = _buildEip712Json({ + chainId: 1, + verifyingContract: address(0), + to: 0xcA11bde05977b3631167028862bE2a173976CA11, + data: _defaultData(), + operation: 1, + nonce: 36 + }); + vm.expectRevert("GnosisSafeHashes: verifyingContract is zero"); + payload.getDomainAndMessageHashFromDataToSign(); + } + + /// forge-config: default.allow_internal_expect_revert = true + function testGetDomainAndMessageHashFromEip712Json_ZeroToAddress() public { + bytes memory payload = _buildEip712Json({ + chainId: 1, + verifyingContract: 0x847B5c174615B1B7fDF770882256e2D3E95b9D92, + to: address(0), + data: _defaultData(), + operation: 1, + nonce: 36 + }); + vm.expectRevert("GnosisSafeHashes: to is zero"); + payload.getDomainAndMessageHashFromDataToSign(); + } + + /// forge-config: default.allow_internal_expect_revert = true + function testGetDomainAndMessageHashFromEip712Json_EmptyJson() public { + bytes memory payload = bytes(""); + vm.expectRevert("GnosisSafeHashes: empty EIP-712 JSON"); + payload.getDomainAndMessageHashFromDataToSign(); + } + /// @notice Test decodeMulticallApproveHash with valid input. function testDecodeMulticallApproveHash_ValidInput() public pure { bytes32 testHash = keccak256("test hash"); From 42877c9f71067699613c0529d03fd5b0e3ecb9b7 Mon Sep 17 00:00:00 2001 From: blaine Date: Mon, 29 Sep 2025 14:33:08 -0400 Subject: [PATCH 4/6] fix: typo --- test/tasks/Regression.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/tasks/Regression.t.sol b/test/tasks/Regression.t.sol index 94761ffb53..e7a9228440 100644 --- a/test/tasks/Regression.t.sol +++ b/test/tasks/Regression.t.sol @@ -563,7 +563,7 @@ contract RegressionTest is Test { /// @notice Expected call data and data to sign generated by manually running the TransferL2PAOFromL1ToEOA template /// Simulate from task directory (test/tasks/example/sep/027-transfer-l2pao-to-eoa) with: - /// SIMULATE_WITHOUT_LEDGER=1 just --dotenv-path "$(pwd)/.env" --justfile ../../../../../src/justfile simulate + /// SIMULATE_WITHOUT_WALLET=1 just --dotenv-path "$(pwd)/.env" --justfile ../../../../../src/justfile simulate function testRegressionCallDataMatches_TransferL2PAOFromL1ToEOA() public { string memory taskConfigFilePath = "test/tasks/example/sep/027-transfer-l2pao-to-eoa/config.toml"; // Call data generated by manually running the TransferL2PAOFromL1ToEOA template on sepolia From af62fd284c324a7fbf523d822a940ced94b33918 Mon Sep 17 00:00:00 2001 From: blaine Date: Mon, 29 Sep 2025 15:32:38 -0400 Subject: [PATCH 5/6] fix: fixing all tests --- src/libraries/GnosisSafeHashes.sol | 13 +++---------- src/tasks/MultisigTask.sol | 6 ++---- test/libraries/GnosisSafeHashes.t.sol | 25 +++++++++++++++++++------ 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/libraries/GnosisSafeHashes.sol b/src/libraries/GnosisSafeHashes.sol index 539724398b..7fff9b5393 100644 --- a/src/libraries/GnosisSafeHashes.sol +++ b/src/libraries/GnosisSafeHashes.sol @@ -8,15 +8,11 @@ import {IMulticall3} from "forge-std/interfaces/IMulticall3.sol"; import {VmSafe} from "forge-std/Vm.sol"; import {IGnosisSafe, Enum} from "@base-contracts/script/universal/IGnosisSafe.sol"; import {Vm} from "forge-std/Vm.sol"; -import {console} from "forge-std/console.sol"; import {stdJson} from "forge-std/StdJson.sol"; /// @title GnosisSafeHashes /// @notice Library for calculating domain separators and message hashes for Gnosis Safe transactions library GnosisSafeHashes { - address internal constant VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); - Vm internal constant vm = Vm(VM_ADDRESS); - // Safe transaction type hash bytes32 constant SAFE_TX_TYPEHASH = keccak256( "SafeTx(address to,uint256 value,bytes data,uint8 operation,uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,address gasToken,address refundReceiver,uint256 nonce)" @@ -172,7 +168,7 @@ library GnosisSafeHashes { /// @notice Reads the result of a call to Safe.encodeTransactionData and returns the message hash. function getDomainAndMessageHashFromDataToSign(bytes memory _dataToSign) internal - pure + view returns (bytes32 domainSeparator_, bytes32 messageHash_) { // If it looks like 0x1901-prefixed encoded bytes (66 bytes total), decode directly. @@ -197,7 +193,7 @@ library GnosisSafeHashes { /// @dev This mirrors the Safe EIP-712 encoding: dynamic types such as bytes are hashed before struct encoding. function getDomainAndMessageHashFromEip712Json(bytes memory _json) internal - pure + view returns (bytes32 domainSeparator_, bytes32 messageHash_) { string memory json = string(_json); @@ -208,9 +204,7 @@ library GnosisSafeHashes { address verifyingContract = stdJson.readAddress(json, ".domain.verifyingContract"); require(verifyingContract != address(0), "GnosisSafeHashes: verifyingContract is zero"); // Compute domain separator according to the JSON schema used in encodeEIP712Json - domainSeparator_ = keccak256( - abi.encode(keccak256("EIP712Domain(uint256 chainId,address verifyingContract)"), chainId, verifyingContract) - ); + domainSeparator_ = calculateDomainSeparator(chainId, verifyingContract); // Message fields address to = stdJson.readAddress(json, ".message.to"); @@ -225,7 +219,6 @@ library GnosisSafeHashes { address gasToken = stdJson.readAddress(json, ".message.gasToken"); address refundReceiver = stdJson.readAddress(json, ".message.refundReceiver"); uint256 nonce = stdJson.readUint(json, ".message.nonce"); - bytes32 dataHash = keccak256(data); messageHash_ = keccak256( abi.encode( diff --git a/src/tasks/MultisigTask.sol b/src/tasks/MultisigTask.sol index f9e0b6c055..ca2fc6d359 100644 --- a/src/tasks/MultisigTask.sol +++ b/src/tasks/MultisigTask.sol @@ -841,10 +841,8 @@ abstract contract MultisigTask is Test, Script, StateOverrideManager, TaskManage GnosisSafeHashes.getDomainAndMessageHashFromDataToSign(dataToSignDataHashes); (bytes32 domainHashFromJson, bytes32 messageHashFromJson) = GnosisSafeHashes.getDomainAndMessageHashFromDataToSign(dataToSignJson); - require( - messageHashFromData == messageHashFromJson && domainHashFromData == domainHashFromJson, - "MultisigTask: EIP712 data to sign hashes do not match. Please report this error." - ); + require(domainHashFromData == domainHashFromJson, "MultisigTask: EIP712 domain hash does not match."); + require(messageHashFromData == messageHashFromJson, "MultisigTask: EIP712 message hash does not match."); return Utils.printDataHashes() ? dataToSignDataHashes : dataToSignJson; } diff --git a/test/libraries/GnosisSafeHashes.t.sol b/test/libraries/GnosisSafeHashes.t.sol index 81a2f150ed..825f2207b2 100644 --- a/test/libraries/GnosisSafeHashes.t.sol +++ b/test/libraries/GnosisSafeHashes.t.sol @@ -147,7 +147,7 @@ contract GnosisSafeHashes_Test is Test { /// @notice Test with valid input. The encoded data is constructed as: /// [0x19, 0x01, 32 bytes domain separator (zeros), 32 bytes message hash]. - function testGetDomainAndMessageHashFromDataToSign_ValidInput() public pure { + function testGetDomainAndMessageHashFromDataToSign_ValidInput() public view { bytes32 expectedDomainSeparator = bytes32(hex"0000000000000000000000000000000000000000000000000000000000001234"); bytes32 expectedMessageHash = bytes32(hex"000000000000000000000000000000000000000000000000000000000000abcd"); bytes memory encodedTxData = abi.encodePacked(bytes2(0x1901), expectedDomainSeparator, expectedMessageHash); @@ -158,7 +158,7 @@ contract GnosisSafeHashes_Test is Test { } /// @notice Test where the message hash is all zeros. - function testGetDomainAndMessageHashFromDataToSign_AllZeros() public pure { + function testGetDomainAndMessageHashFromDataToSign_AllZeros() public view { bytes memory encodedTxData = abi.encodePacked(bytes2(0x1901), bytes32(0), bytes32(0)); bytes32 expectedHash = bytes32(0); @@ -167,7 +167,7 @@ contract GnosisSafeHashes_Test is Test { assertEq(messageHash, expectedHash, "Message hash should be all zeros"); } - function testGetDomainAndMessageHashFromDataToSign_MaxUint256() public pure { + function testGetDomainAndMessageHashFromDataToSign_MaxUint256() public view { bytes memory encodedTxData = abi.encodePacked(bytes2(0x1901), bytes32(type(uint256).max), bytes32(type(uint256).max)); bytes32 expectedHash = bytes32(type(uint256).max); @@ -193,13 +193,14 @@ contract GnosisSafeHashes_Test is Test { encodedTxData.getDomainAndMessageHashFromDataToSign(); } - function testGetDomainAndMessageHashFromDataToSign_FuzzTest(bytes32 randomHash) public pure { + function testGetDomainAndMessageHashFromDataToSign_FuzzTest(bytes32 randomHash) public view { bytes memory encodedTxData = abi.encodePacked(bytes2(0x1901), bytes32(0), randomHash); (, bytes32 messageHash) = encodedTxData.getDomainAndMessageHashFromDataToSign(); assertEq(messageHash, randomHash, "Message hash should match the input random hash"); } function testGetDomainAndMessageHashFromEip712Json() public { + vm.createSelectFork("mainnet", 23470688); bytes memory payload = _buildEip712Json({ chainId: 1, verifyingContract: 0x847B5c174615B1B7fDF770882256e2D3E95b9D92, @@ -221,6 +222,8 @@ contract GnosisSafeHashes_Test is Test { /// forge-config: default.allow_internal_expect_revert = true function testGetDomainAndMessageHashFromEip712Json_InvalidOperation() public { + vm.createSelectFork("mainnet", 23470688); + GnosisSafeHashes_Harness gnosisSafeHashesHarness = new GnosisSafeHashes_Harness(); bytes memory payload = _buildEip712Json({ chainId: 1, verifyingContract: 0x847B5c174615B1B7fDF770882256e2D3E95b9D92, @@ -230,7 +233,7 @@ contract GnosisSafeHashes_Test is Test { nonce: 36 }); vm.expectRevert("GnosisSafeHashes: invalid operation, only DelegateCall is supported"); - payload.getDomainAndMessageHashFromDataToSign(); + gnosisSafeHashesHarness.getDomainAndMessageHashFromDataToSign(payload); } /// forge-config: default.allow_internal_expect_revert = true @@ -249,6 +252,8 @@ contract GnosisSafeHashes_Test is Test { /// forge-config: default.allow_internal_expect_revert = true function testGetDomainAndMessageHashFromEip712Json_ZeroToAddress() public { + vm.createSelectFork("mainnet", 23470688); + GnosisSafeHashes_Harness gnosisSafeHashesHarness = new GnosisSafeHashes_Harness(); bytes memory payload = _buildEip712Json({ chainId: 1, verifyingContract: 0x847B5c174615B1B7fDF770882256e2D3E95b9D92, @@ -258,7 +263,7 @@ contract GnosisSafeHashes_Test is Test { nonce: 36 }); vm.expectRevert("GnosisSafeHashes: to is zero"); - payload.getDomainAndMessageHashFromDataToSign(); + gnosisSafeHashesHarness.getDomainAndMessageHashFromDataToSign(payload); } /// forge-config: default.allow_internal_expect_revert = true @@ -454,4 +459,12 @@ contract GnosisSafeHashes_Harness is Test { function getOperationDetails(VmSafe.AccountAccessKind _kind) public pure returns (string memory, Enum.Operation) { return GnosisSafeHashes.getOperationDetails(_kind); } + + function getDomainAndMessageHashFromDataToSign(bytes memory payload) + public + view + returns (bytes32 domainSeparator, bytes32 messageHash) + { + return GnosisSafeHashes.getDomainAndMessageHashFromDataToSign(payload); + } } From 43ecd554732f7cbc92864d4f7f9ee40ee2bbe8c6 Mon Sep 17 00:00:00 2001 From: blaine Date: Tue, 30 Sep 2025 13:34:30 -0400 Subject: [PATCH 6/6] fix: docs update and setting env var --- README.md | 4 +--- src/justfile | 5 +++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d33bf0760d..491de10839 100644 --- a/README.md +++ b/README.md @@ -196,10 +196,8 @@ just sign-stack [child-safe-name-depth-1] [child-safe-name-dept **Environment variables:** - `HD_PATH` - Hardware wallet derivation path index (default: 0). The value is inserted into the [BIP44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) Ethereum path as `m/44'/60'/$HD_PATH'/0/0` (e.g. `0` -> `m/44'/60'/0'/0/0`, `1` -> `m/44'/60'/1'/0/0`). Use this to select the desired Ethereum account on your hardware wallet. -- `USE_KEYSTORE` - If set, uses keystore instead of ledger. By default, keys are stored under `~/.foundry/keystores`. - `SIMULATE_WITHOUT_WALLET` - Set to run the simulation phase without a wallet (i.e. Ledger/Trezor/KeyStore). This step will simulate a task with valid, default keys. -- `WALLET_TYPE` - This can be either `ledger`, `trezor` or `keystore`. The default value is `ledger`. -- `USE_KEYSTORE` - If set, uses keystore instead of ledger. By default, keys are stored under `~/.foundry/keystores`. +- `WALLET_TYPE` - This can be either `ledger`, `trezor` or `keystore` (By default, keys are stored under `~/.foundry/keystores`). The default value is `ledger`. - `EIP712_ENCODED_DATA_TO_SIGN` - If set the encoded EIP-712 typed data JSON is used for signing; when unset, only the EIP-712 domain and message hashes (default) are used for signing. **Examples:** diff --git a/src/justfile b/src/justfile index 8c12014347..17fce62b7a 100644 --- a/src/justfile +++ b/src/justfile @@ -381,6 +381,11 @@ _get-signer-args hdPath='0' wallet_type='ledger': root_dir=$(git rev-parse --show-toplevel) root_just_file="${root_dir}/src/justfile" + # The 'eip712sign' library requires EIP712 typed data to be supplied for Trezor devices. + if [ "{{wallet_type}}" = "trezor" ]; then + export EIP712_ENCODED_DATA_TO_SIGN=true + fi + if [ "{{wallet_type}}" != "keystore" ]; then # either 'ledger' or 'trezor' hdpaths="m/44'/60'/{{hdPath}}'/0/0" echo "Using {{wallet_type}}" >&2