GitHub - pcaversaccio/safe-tx-hashes-util: This Bash script calculates the Safe transaction hashes by retrieving transaction details from the Safe transaction service API and computing both the domain and message hashes using the EIP-712 standard. (original) (raw)

Safe Multisig Transaction Hashes

👮‍♂️ Sanity checks License: AGPL-3.0-only

|)0//'T TR|_|5T, /3R1FY! 🫡

This Bash script calculates the Safe transaction hashes by retrieving transaction details from the Safe transaction service API and computing both the domain and message hashes using the EIP-712 standard.

Note

This Bash script relies on the Safe transaction service API, which requires transactions to be proposed and logged in the service before they can be retrieved. Consequently, the initial transaction proposer cannot access the transaction at the proposal stage, making this approach incompatible with 1-of-1 multisigs.1 A simple and effective solution is to use the --interactive mode, which gracefully defaults to zero values when no transaction is logged, allowing you to fully customise all transaction parameters.

Important

All Safe multisig versions starting from 0.1.0 and newer are supported.

Security Best Practices for Using This Script

Read This Before Proceeding!

Supported Networks

Usage

Note

Ensure that cast and chisel are installed locally. For installation instructions, refer to this guide. This script is designed to work with the latest stable versions of cast and chisel, starting from version 1.2.2.

Tip

For macOS users, please refer to the macOS Users: Upgrading Bash section.

./safe_hashes.sh [--help] [--version] [--list-networks] --network --address

[--nonce ] [--nested-safe-address
] [--nested-safe-nonce ] [--message ] [--interactive]

Options:

Note

Please note that --help, --version, and --list-networks can be used independently or alongside other options without causing the script to fail. They are special options that can be called without affecting the rest of the command processing.

Before you invoke the script, make it executable:

Tip

The script is already set as executable in the repository, so you can run it immediately after cloning or pulling the repository without needing to change permissions.

If you feel fancy, you can also try:

curl -fsSL https://raw.githubusercontent.com/pcaversaccio/safe-tx-hashes-util/main/install.sh | bash

To enable debug mode, set the DEBUG environment variable to true before running the script:

DEBUG=true ./safe_hashes.sh ...

This will print each command before it is executed, which is helpful when troubleshooting.

The colour output is auto-detected and can be controlled with:

NO_COLOR=true ./safe_hashes.sh ...

FORCE_COLOR=true ./safe_hashes.sh ...

Only the exact value true is accepted to avoid accidental activation. If both are set, NO_COLOR takes precedence and disables all formatting. Otherwise, colour is enabled only if output is to a terminal, tput is available, and the terminal supports at least the 8 standard ANSI colours.

macOS Users: Upgrading Bash

This script requires Bash 4.0 or higher due to its use of associative arrays (introduced in Bash 4.0). Unfortunately, macOS ships by default with Bash 3.2 due to licensing requirements. To use this script, install a newer version of Bash through Homebrew:

  1. Install Homebrew if you haven't already:

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

  1. Install the latest version of Bash:
  2. Verify that you are using Bash version 4.0 or higher:

Optional: Set the New Bash as Your Default Shell

  1. Find the path to your Bash installation (BASH_PATH):
  2. Add the new shell to the list of allowed shells:

Depending on your Mac's architecture and where Homebrew installs Bash, you will use one of the following commands:

For Intel-based Macs or if Homebrew is installed in the default location.

sudo bash -c 'echo /usr/local/bin/bash >> /etc/shells'

or

For Apple Silicon (M1/M2) Macs or if you installed Homebrew using the default path for Apple Silicon.

sudo bash -c 'echo /opt/homebrew/bin/bash >> /etc/shells'

  1. Set the new Bash as your default shell:

Make sure to replace BASH_PATH with the actual path you retrieved in step 1.

Safe Transaction Hashes

To calculate the Safe transaction hashes for a specific transaction, you need to specify the network, address, and nonce parameters. An example:

./safe_hashes.sh --network arbitrum --address 0x111CEEee040739fD91D29C34C33E6B3E112F2177 --nonce 234

The script will output the domain, message, and Safe transaction hashes, allowing you to easily verify them against the values displayed on your Ledger hardware wallet screen:

=================================== = Selected Network Configurations =

Network: arbitrum Chain ID: 42161

======================================== = Transaction Data and Computed Hashes =

Transaction Data: Multisig address: 0x111CEEee040739fD91D29C34C33E6B3E112F2177 To: 0x111CEEee040739fD91D29C34C33E6B3E112F2177 Value: 0 Data: 0x0d582f130000000000000000000000000c75fa5a5f1c0997e3eea425cfa13184ed0ec9e50000000000000000000000000000000000000000000000000000000000000003 Operation: Call Safe Transaction Gas: 0 Base Gas: 0 Gas Price: 0 Gas Token: 0x0000000000000000000000000000000000000000 Refund Receiver: 0x0000000000000000000000000000000000000000 Nonce: 234 Encoded message: 0xbb8310d486368db6bd6f849402fdd73ad53d316b5a4b2644ad6efe0f941286d8000000000000000000000000111ceeee040739fd91d29c34c33e6b3e112f21770000000000000000000000000000000000000000000000000000000000000000b34f85cea7c4d9f384d502fc86474cd71ff27a674d785ebd23a4387871b8cbfe00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ea Method: addOwnerWithThreshold Parameters: [ { "name": "owner", "type": "address", "value": "0x0c75Fa5a5F1C0997e3eEA425cFA13184ed0eC9e5" }, { "name": "_threshold", "type": "uint256", "value": "3" } ]

WARNING: The "addOwnerWithThreshold" function modifies the owners or threshold of the Safe. Proceed with caution!

Hashes: Domain hash: 0x1CF7F9B1EFE3BC47FE02FD27C649FEA19E79D66040683A1C86C7490C80BF7291 Message hash: 0xD9109EA63C50ECD3B80B6B27ED5C5A9FD3D546C2169DFB69BFA7BA24CD14C7A5 Safe transaction hash: 0x0cb7250b8becd7069223c54e2839feaed4cee156363fbfe5dd0a48e75c4e25b3

To see an example of a standard ETH transfer, run the command: ./safe_hashes.sh --network ethereum --address 0x8FA3b4570B4C96f8036C13b64971BA65867eEB48 --nonce 39 and review the output.

To list all supported networks:

./safe_hashes.sh --list-networks

Interactive Mode

Warning

If it's not already obvious: This is YOLO mode – BE VERY CAREFUL!

When using --interactive mode, you will be prompted to provide values for various parameters such as version, to, value, and others. If you leave any parameter empty, the default value displayed in the terminal will be used. These defaults are either retrieved from the Safe transaction service API or, in case of failure, fall back to zero values. This allows you to customise the parameters or proceed with the API-sourced defaults.

Read This Before Proceeding:

As an example, invoke the following command:

./safe_hashes.sh --network arbitrum --address 0x111CEEee040739fD91D29C34C33E6B3E112F2177 --nonce 234 --interactive

The final output will look like this:

Interactive mode is enabled. You will be prompted to enter values for parameters such as version, to, value, and others.

If it's not already obvious: This is YOLO mode – BE VERY CAREFUL!

IMPORTANT:

Enter the Safe multisig version (default: 1.3.0+L2): Enter the to address (default: 0x111CEEee040739fD91D29C34C33E6B3E112F2177): Enter the value (default: 0): 1000 Enter the data (default: 0x0d582f130000000000000000000000000c75fa5a5f1c0997e3eea425cfa13184ed0ec9e50000000000000000000000000000000000000000000000000000000000000003): Enter the operation (default: 0; 0 = CALL, 1 = DELEGATECALL): 1 Enter the safeTxGas (default: 0): Enter the baseGas (default: 0): Enter the gasPrice (default: 0): 50 Enter the gasToken (default: 0x0000000000000000000000000000000000000000): 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 Enter the refundReceiver (default: 0x0000000000000000000000000000000000000000): 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045

WARNING: The transaction includes an untrusted delegate call to address 0x111CEEee040739fD91D29C34C33E6B3E112F2177! This may lead to unexpected behaviour or vulnerabilities. Please review it carefully before you sign!

WARNING: This transaction uses a custom gas token and a custom refund receiver. This combination can be used to hide a rerouting of funds through gas refunds. Furthermore, the gas price is non-zero, which increases the potential for hidden value transfers.

=================================== = Selected Network Configurations =

Network: arbitrum Chain ID: 42161

======================================== = Transaction Data and Computed Hashes =

Transaction Data: Multisig address: 0x111CEEee040739fD91D29C34C33E6B3E112F2177 To: 0x111CEEee040739fD91D29C34C33E6B3E112F2177 Value: 1000 Data: 0x0d582f130000000000000000000000000c75fa5a5f1c0997e3eea425cfa13184ed0ec9e50000000000000000000000000000000000000000000000000000000000000003 Operation: Delegatecall (UNTRUSTED delegatecall; carefully verify before proceeding!) Safe Transaction Gas: 0 Base Gas: 0 Gas Price: 50 Gas Token: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 Refund Receiver: 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 Nonce: 234 Encoded message: 0xbb8310d486368db6bd6f849402fdd73ad53d316b5a4b2644ad6efe0f941286d8000000000000000000000000111ceeee040739fd91d29c34c33e6b3e112f217700000000000000000000000000000000000000000000000000000000000003e8b34f85cea7c4d9f384d502fc86474cd71ff27a674d785ebd23a4387871b8cbfe0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000032000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa9604500000000000000000000000000000000000000000000000000000000000000ea Method: Unavailable in interactive mode Parameters: Unavailable in interactive mode

Hashes: Domain hash: 0x1CF7F9B1EFE3BC47FE02FD27C649FEA19E79D66040683A1C86C7490C80BF7291 Message hash: 0xC7E826933DA60E6AC3E2246ED0563A26A920A65BEAA9089D784AC96234141BB3 Safe transaction hash: 0xc818fceb1cace51c1a4039c4c66fc73d95eccc298104c9c52debac604b9f4e04

Nested Safes

This script supports calculating the Safe transaction hashes for nested Safe (i.e. use a Safe as a signatory to another Safe) approval transactions. When a nested Safe needs to approve a transaction on the primary Safe, it must call the approveHash(bytes32) function on the target Safe with the Safe transaction hash to approve:

function approveHash(bytes32 hashToApprove) external override { if (owners[msg.sender] == address(0)) revertWithError("GS030"); approvedHashes[msg.sender][hashToApprove] = 1; emit ApproveHash(hashToApprove, msg.sender); }

To calculate both the primary transaction hash and the nested Safe approveHash transaction hash, specify the network, address, nonce, nested-safe-address, and nested-safe-nonce parameters:

./safe_hashes.sh --network sepolia --address 0x657ff0D4eC65D82b2bC1247b0a558bcd2f80A0f1 --nonce 4 --nested-safe-address 0x6bc56d6CE87C86CB0756c616bECFD3Cd32b09251 --nested-safe-nonce 4

The script will first calculate and display the primary transaction hashes. Then, it will construct and calculate the hashes for the approveHash transaction:

=================================== = Selected Network Configurations =

Network: sepolia Chain ID: 11155111

======================================== = Transaction Data and Computed Hashes =

Primary Safe Transaction Data and Computed Hashes

Transaction Data: Multisig address: 0x657ff0D4eC65D82b2bC1247b0a558bcd2f80A0f1 To: 0x255C3912f91eF11bFDadd405F13144a823Da8cc5 Value: 100000000000000000 Data: 0x Operation: Call Safe Transaction Gas: 0 Base Gas: 0 Gas Price: 0 Gas Token: 0x0000000000000000000000000000000000000000 Refund Receiver: 0x0000000000000000000000000000000000000000 Nonce: 4 Encoded message: 0xbb8310d486368db6bd6f849402fdd73ad53d316b5a4b2644ad6efe0f941286d8000000000000000000000000255c3912f91ef11bfdadd405f13144a823da8cc5000000000000000000000000000000000000000000000000016345785d8a0000c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a4700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004 Method: 0x (ETH Transfer) Parameters: []

Hashes: Domain hash: 0x611379C19940CAEE095CDB12BEBE6A9FA9ABB74CDB1FBD7377C49A1F198DC24F Message hash: 0x565BBA8B51924FFA64953596D0A2DD5C2CAD39649F7DE0BF2C8DBC903BD03258 Safe transaction hash: 0xcb8bbe7bf8f8a1f3f57658e450d07d4422356ac042d96a87ba425b19e67a78a1

Nested Safe approveHash Transaction Data and Computed Hashes

The specified nested Safe at 0x6bc56d6CE87C86CB0756c616bECFD3Cd32b09251 will use the following transaction to approve the primary transaction.

Transaction Data: Multisig address: 0x6bc56d6CE87C86CB0756c616bECFD3Cd32b09251 To: 0x657ff0D4eC65D82b2bC1247b0a558bcd2f80A0f1 Value: 0 Data: 0xd4d9bdcdcb8bbe7bf8f8a1f3f57658e450d07d4422356ac042d96a87ba425b19e67a78a1 Operation: Call Safe Transaction Gas: 0 Base Gas: 0 Gas Price: 0 Gas Token: 0x0000000000000000000000000000000000000000 Refund Receiver: 0x0000000000000000000000000000000000000000 Nonce: 4 Encoded message: 0xbb8310d486368db6bd6f849402fdd73ad53d316b5a4b2644ad6efe0f941286d8000000000000000000000000657ff0d4ec65d82b2bc1247b0a558bcd2f80a0f10000000000000000000000000000000000000000000000000000000000000000873d41be4be44b68a3ad9cb19bf644be0f02392498d3a81d46d9f0741c9426640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004 Method: approveHash Parameters: [ { "name": "hashToApprove", "type": "bytes32", "value": "0xcb8bbe7bf8f8a1f3f57658e450d07d4422356ac042d96a87ba425b19e67a78a1" } ]

Hashes: Domain hash: 0x55F6C329A7834E2A4E789F5526F328FA75D14FE75B97B0001BE40CAF46CA92A1 Message hash: 0xCD411EE5D49344391EF8D37B76E19DFACF505BBB20E856AC907ACB5958ECBDF0 Safe transaction hash: 0x86eb3f93f2670d119a4ecb8eeaa4dafe31a28abcafe06688d47e195a3dd7abb0

The nested Safe approveHash transaction is constructed with the following parameters:

Note

The --interactive mode supports nested Safe transactions but only allows overriding the nested Safe version, not other transaction values in the approveHash transaction.

Safe Message Hashes

Important

At present, this script does not support calculating Safe message hashes for EIP-712-based messages due to the inherent complexity of parsing the message and identifying the relevant type hashes. However, you can find my easily adjustable Bash script version here to calculate Safe message hashes for EIP-712-based messages.

This script not only calculates Safe transaction hashes but also supports computing the corresponding hashes for off-chain messages following the EIP-712 standard. To calculate the Safe message hashes for a specific message, specify the network, address, and message parameters. The message parameter must specify a valid file containing the raw message. This can be either the file name or a relative path (e.g., path/to/message.txt). Note that the script normalises line endings to LF (\n) in the message file.

An example: Save the following message to a file named message.txt:

Welcome to OpenSea!

Click to sign in and accept the OpenSea Terms of Service (https://opensea.io/tos) and Privacy Policy (https://opensea.io/privacy).

This request will not trigger a blockchain transaction or cost any gas fees.

Wallet address: 0x657ff0d4ec65d82b2bc1247b0a558bcd2f80a0f1

Nonce: ea499f2f-fdbc-4d04-92c4-b60aba887e06

Then, invoke the following command:

./safe_hashes.sh --network sepolia --address 0x657ff0D4eC65D82b2bC1247b0a558bcd2f80A0f1 --message message.txt

The script will output the raw message, along with the domain, message, and Safe message hashes, allowing you to easily verify them against the values displayed on your Ledger hardware wallet screen:

=================================== = Selected Network Configurations =

Network: sepolia Chain ID: 11155111

==================================== = Message Data and Computed Hashes =

Message Data: Multisig address: 0x657ff0D4eC65D82b2bC1247b0a558bcd2f80A0f1 Message: Welcome to OpenSea!

Click to sign in and accept the OpenSea Terms of Service (https://opensea.io/tos) and Privacy Policy (https://opensea.io/privacy).

This request will not trigger a blockchain transaction or cost any gas fees.

Wallet address: 0x657ff0d4ec65d82b2bc1247b0a558bcd2f80a0f1

Nonce: ea499f2f-fdbc-4d04-92c4-b60aba887e06

Hashes: Safe message: 0xcb1a9208c1a7c191185938c7d304ed01db68677eea4e689d688469aa72e34236 Domain hash: 0x611379C19940CAEE095CDB12BEBE6A9FA9ABB74CDB1FBD7377C49A1F198DC24F Message hash: 0xA5D2F507A16279357446768DB4BD47A03BCA0B6ACAC4632A4C2C96AF20D6F6E5 Safe message hash: 0x1866b559f56261ada63528391b93a1fe8e2e33baf7cace94fc6b42202d16ea08

Note

The --interactive mode is not supported when calculating Safe message hashes. If using a nested Safe as the signer for the primary message, you must provide the --nested-safe-address argument along with the other parameters to retrieve the additional computed hashes for the nested Safe.

Trust Assumptions

  1. You trust my script 😃.
  2. You trust Linux.
  3. You trust Foundry.
  4. You trust the Safe transaction service API.
  5. You trust Ledger's secure screen.

Important

You can remove the trust assumption "4. You trust the Safe transaction service API." by enabling --interactive mode and verifying the calldata independently (this should always be done!).

Community-Maintained User Interface Implementations

Important

Please be aware that user interface implementations may introduce additional trust assumptions, such as relying on npm dependencies that have not undergone thorough review or a deployment process that could be compromised by an attacker. Always verify and cross-reference with the main script.

💸 Donation

I am a strong advocate of the open-source and free software paradigm. However, if you feel my work deserves a donation, you can send it to this address: 0xe9Fa0c8B5d7F79DeC36D3F448B1Ac4cEdedE4e69. I can pledge that I will use this money to help fix more existing challenges in the Ethereum ecosystem 🤝.

  1. It is theoretically possible to query transactions prior to the first signature; however, this functionality is not incorporated into the main script. To do so, you would proceed through the Safe UI as usual, stopping at the page where the transaction is signed or executed. At this point, the action is recorded in the Safe Transaction Service API, allowing you to retrieve the unsigned transaction by setting trusted=false in the API query within your Bash script. For example, you might use a query such as: https://safe-transaction-arbitrum.safe.global/api/v2/safes/0xB24A3AA250E209bC95A4a9afFDF10c6D099B3d34/multisig-transactions/?trusted=false&nonce=4. This decision to not implement this feature avoids potential confusion caused by unsigned transactions in the queue, especially when multiple transactions share the same nonce, making it unclear which one to act upon. If this feature aligns with your needs, feel free to fork the script and modify it as necessary.