# Verifying Multisig Transactions with Ledger (DRAFT) This guide explains the recommended multisig transaction signing workflow. One of the key steps in safe transaction verification is calldata hash verification. By verifying the hashes on the UI of the multisig compared to the hashes displayed on a connected hardware wallet we are protected from an attack where the UI is compromised or an unexpected malicious transaction is injected into the signing flow. This type of attack resulted in the $1.5B loss experienced by Bybit. ## Tools & Definitions [Safe Tx Hashes Util (CLI)](https://github.com/pcaversaccio/safe-tx-hashes-util) - CLI tool for verifying calldata in multisig transactions * This tool is the most actively supported and feature-rich utility for Safe transaction verification [OpenZeppelin Safe Utils (UI)](https://safeutils.openzeppelin.com/) - Hosted frontend for verifying calldata based on the Safe Tx Hashes Util CLI tool * This tool provides a simpler way to generate the hashes used in signature validation * However it tends to lag behind the features available in the CLI tool. Notably this tool cannot yet verify signatures and does not provide warnings for critical actions. Domain Hash - EIP712 domain separator. This is unique to each Safe Message Hash - Raw hash of the transaction parameters. **THIS is what will actually appear on the hardware wallet screen** SafeTxHash - Combined hash of domain hash and message hash. **THIS is what a nested Safe uses to approve a hash on a primary Safe** ## Comparing Verification Methods | Feature | Safe Tx Hashes Util (CLI) | UI Tool | | -------- | -------- | -------- | | Calldata Verification | ✅ | ✅ | | Safe API Support | ✅ | ✅ | | Manual Input Support | ✅ | ✅ | | Nested Safe Support | ✅ | ✅ | | Signature Verification | ✅ | 🚫 | | Warnings For Risky Actions | ✅ | 🚫 | ## Transaction workflow ### Connecting wallet Use hardware wallet connected to browser wallet connected to Safe UI **Delegated Proposer** - It is recommended, but not required to authorize a separate transaction proposer for a Safe. This address can prepare transactions for signers to sign but is not an authorized signer on the Safe. Therefore **there is no risk of malicious signatures which can affect the Safe assets**. This wallet can hold no funds and simply act as a proposer. The primary reason to have a delegated proposer is that the hash verification utilities depend on the Safe API (unless details are entered manually). Until a transaction is **proposed** it does not show up in the API so the hash verification tools cannot detect it. ### Transaction preparation Use Safe UI to send assets, prepare a bundle of transactions, use Safe apps, or WalletConnect to external apps. Before signing the Safe UI will show the expected hashes. However this is not sufficient for full verification in case the UI has been compromised. ### Simulation The Safe UI will generate a link to a Tenderly simulation of the transaction. It is highly recommended to perform this simulation and check if the expected events and asset transfers are present in the simulation. **NOTE** this is made more complicated if the Safe is executing transactions through a timelock. ### Hash verification It is **essential** to verify the hashes of the expected transaction before signing the proposal. On either the UI tool or the CLI tool enter the Safe address, network, and Safe transaction nonce. The tools will output 3 hashes. The *messageHash* will be the hash that shows up on the signing device. The CLI tool will display warnings in case the transaction hash high risk to the Safe configuration like changing owners, thresholds, or upgrading the underlying contract. In addition to verifying the hash it is **essential** to also review the decoded calldata in the tool. This will indicate if the transaction coming from the Safe API matches the expected prepared transaction. **NOTE** both the UI tool and the CLI tool unfortunately expect the address to be in its checksum form. - Not 0xa79c6968e3c75ae4ef388370d1f142720d498fec but 0xA79C6968E3c75aE4eF388370d1f142720D498fEC - Both tools will return errors if the non-checksummed version is used ### Signing It is recommended to sign, but not execute the transaction even if you are the last signer required to execute the transaction. If you execute the transaction directly the hardware wallet screen will not show the expected hash. ### Executing Transaction **Delegated Executor** - Once a Safe transaction is fully signed, any connected address can execute the transaction. It is recommended, but not required to use an address which is not a signer on the Safe to execute fully signed tranasctions. ## Example Transaction Workflow In this example we will follow a transaction execution ### Transaction Preparation This screenshot shows the preparation of a simple USDC transfer by a delegated proposer. ![Screenshot 2025-06-02 at 11.33.39](https://hackmd.io/_uploads/rk9Q9Bizgg.png) ![Screenshot 2025-06-02 at 11.33.56](https://hackmd.io/_uploads/ByKX9Sizlx.png) ### Transaction Simulation The simulation below shows the results of a simple ERC20 transfer transaction ![Screenshot 2025-06-02 at 11.35.46](https://hackmd.io/_uploads/HyGI9BjMgl.png) ### Hash Verification #### CLI Tool Input ``` ./safe_hashes.sh --network base --address 0xA79C6968E3c75aE4eF388370d1f142720D498fEC --nonce 1 ``` Output ``` Transaction Data Multisig address: 0xA79C6968E3c75aE4eF388370d1f142720D498fEC To: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 Value: 0 Data: 0xa9059cbb00000000000000000000000040bb32f0862d227813c86fd83a7b3c9db9074e2a00000000000000000000000000000000000000000000000000000000001e8480 Operation: Call Safe Transaction Gas: 0 Base Gas: 0 Gas Price: 0 Gas Token: 0x0000000000000000000000000000000000000000 Refund Receiver: 0x0000000000000000000000000000000000000000 Nonce: 1 Encoded message: 0xbb8310d486368db6bd6f849402fdd73ad53d316b5a4b2644ad6efe0f941286d8000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000000000000000000005d0ba510fe537fe693c84b0f84fcf3a663a8e2132216e4608f37b45bf35adf5a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001 Method: transfer Parameters: [ { "name": "to", "type": "address", "value": "0x40bB32F0862D227813c86FD83A7B3c9db9074E2A" }, { "name": "value", "type": "uint256", "value": "2000000" } ] Hashes Domain hash: 0xDA7587332BD69943F215544904DE40FA194F588ECF7C6961DEE4F509E0C095E0 Message hash: 0x743A70B48CB075DC306720C1C0AFFBA79F05250D9046F25BF98ACC72107665BB Safe transaction hash: 0xb660296f6721efd77003d6f47902426d738341970bc818861432a5362e739cb6 ``` In the output above we can see the decoded calldata of the ERC20 transfer, and the relevant hashes. The **Message Hash** is the one that will appear on the device. #### UI Tool Similarly when we use the UI tool we can see the expected hashes and the decoded calldata. ![Screenshot 2025-06-06 at 11.57.21](https://hackmd.io/_uploads/rJHblqeXee.png) #### Signing Before we sign on the Ledger we can see the same hashes on the Safe UI ![Screenshot 2025-06-02 at 11.38.32](https://hackmd.io/_uploads/rkR7lclXxg.png) When signing the hash (but NOT executing) the message hash appears on the Ledger screen ![IMG_6515](https://hackmd.io/_uploads/HJTSJ9xmeg.jpg) ![IMG_6516](https://hackmd.io/_uploads/HypBkceQgl.jpg) ### Transaction Execution Finally we can connect to the Safe UI from *any* address and execute the signed transaction. ![Screenshot 2025-06-06 at 12.02.03](https://hackmd.io/_uploads/HJjfZ9g7gl.png) ## Special cases ### Nested Safe For nexted safes (where a Safe is a signer on another Safe) the wallet connected to the Safe will show a different hash than the one displayed above. This hash is related to the function `approveHash(safeTxHash)`. ### 1/1 Safe For 1/1 Safes it is highly recommended to use a delegated proposer so the transaction goes into the queue and can be found via the Safe TX API ### Protocol specific tooling For critical protocol multisigs like Security Councils it is recommended to