diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index fc55a05..6c80641 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -15,6 +15,8 @@ jobs: - ubuntu-latest node_version: - 22 + go_version: + - 1.24 steps: - name: Checkout @@ -25,9 +27,21 @@ jobs: with: node-version: ${{ matrix.node_version }} + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go_version }} + cache: False + + - name: Install `shfmt` + run: go install mvdan.cc/sh/v3/cmd/shfmt@latest + - name: Run Prettier run: npx prettier -c '**/*.{md,yml,yaml}' + - name: Run `shfmt` + run: shfmt -d safe_hashes.sh + codespell: runs-on: ${{ matrix.os }} strategy: diff --git a/README.md b/README.md index 56be250..2c35441 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ This Bash [script](./safe_hashes.sh) calculates the Safe transaction hashes by r - [Optional: Set the New Bash as Your Default Shell](#optional-set-the-new-bash-as-your-default-shell) - [Safe Transaction Hashes](#safe-transaction-hashes) - [Interactive Mode](#interactive-mode) + - [Nested Safes](#nested-safes) - [Safe Message Hashes](#safe-message-hashes) - [Trust Assumptions](#trust-assumptions) - [Community-Maintained User Interface Implementations](#community-maintained-user-interface-implementations) @@ -68,7 +69,9 @@ This Bash [script](./safe_hashes.sh) calculates the Safe transaction hashes by r > For macOS users, please refer to the [macOS Users: Upgrading Bash](#macos-users-upgrading-bash) section. ```console -./safe_hashes.sh [--help] [--version] [--list-networks] --network <network> --address <address> [--nonce <nonce>] [--message <file>] [--interactive] +./safe_hashes.sh [--help] [--version] [--list-networks] --network <network> --address <address> + [--nonce <nonce>] [--nested-safe-address <address>] [--nested-safe-nonce <nonce>] + [--message <file>] [--interactive] ``` **Options:** @@ -76,9 +79,11 @@ This Bash [script](./safe_hashes.sh) calculates the Safe transaction hashes by r - `--help`: Display this help message. - `--version`: Display the latest local commit hash (=version) of the script. - `--list-networks`: List all supported networks and their chain IDs. -- `--network <network>`: Specify the network (e.g., `ethereum`, `polygon`). -- `--address <address>`: Specify the Safe multisig address. +- `--network <network>`: Specify the network (e.g., `ethereum`, `polygon`) (**required**). +- `--address <address>`: Specify the Safe multisig address (**required**). - `--nonce <nonce>`: Specify the transaction nonce (required for transaction hashes). +- `--nested-safe-address <address>`: Specify the nested Safe multisig address (optional for transaction hashes or off-chain message hashes). +- `--nested-safe-nonce <nonce>`: Specify the nonce for the nested Safe transaction (optional for transaction hashes). - `--message <file>`: Specify the message file (required for off-chain message hashes). - `--interactive`: Use the interactive mode (optional for transaction hashes). @@ -307,6 +312,104 @@ Message hash: 0xC7E826933DA60E6AC3E2246ED0563A26A920A65BEAA9089D784AC96234141BB3 Safe transaction hash: 0xc818fceb1cace51c1a4039c4c66fc73d95eccc298104c9c52debac604b9f4e04 ``` +### Nested Safes + +This [script](./safe_hashes.sh) 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)`](https://github.com/safe-global/safe-smart-account/blob/bdcfce3a76c4d1dfb256ac2ca971be7cfd6e493a/contracts/Safe.sol#L372-L379) function on the target Safe with the Safe transaction hash to approve: + +```solidity +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: + +```console +./safe_hashes.sh --network sepolia --address 0x657ff0D4eC65D82b2bC1247b0a558bcd2f80A0f1 --nonce 4 --nested-safe-address 0x6bc56d6CE87C86CB0756c616bECFD3Cd32b09251 --nested-safe-nonce 4 +``` + +The [script](./safe_hashes.sh) will first calculate and display the primary transaction hashes. Then, it will construct and calculate the hashes for the `approveHash` transaction: + +```console +=================================== += 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: + +- `to`: The primary Safe multisig address. +- `data`: Encoded `approveHash(bytes32)` function call with the Safe transaction hash as argument. +- `value`: Set to `0`. +- `operation`: Set to `0` (i.e. `CALL`). +- All other parameters are set to their default values (`0` or the zero address `0x0000000000000000000000000000000000000000`). + +> [!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] @@ -365,12 +468,15 @@ Nonce: ea499f2f-fdbc-4d04-92c4-b60aba887e06 > Hashes: -Raw message hash: 0xcb1a9208c1a7c191185938c7d304ed01db68677eea4e689d688469aa72e34236 +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](./safe_hashes.sh) 😃. diff --git a/safe_hashes.sh b/safe_hashes.sh index d73f6ac..2561fe7 100755 --- a/safe_hashes.sh +++ b/safe_hashes.sh @@ -16,33 +16,33 @@ readonly RESET="\e[0m" # Check the Bash version compatibility. if [[ "${BASH_VERSINFO[0]:-0}" -lt 4 ]]; then - echo -e "${BOLD}${RED}Error: This script requires Bash 4.0 or higher.${RESET}" - echo -e "${BOLD}${RED}Current version: $BASH_VERSION${RESET}" - echo -e "${BOLD}${RED}Please upgrade your Bash installation.${RESET}" - echo -e "${BOLD}${RED}If you've already upgraded via Homebrew, try running:${RESET}" - echo -e "${BOLD}${RED}/opt/homebrew/bin/bash $0 $@${RESET}" - exit 1 + echo -e "${BOLD}${RED}Error: This script requires Bash 4.0 or higher!${RESET}" + echo -e "${BOLD}${RED}Current version: $BASH_VERSION${RESET}" + echo -e "${BOLD}${RED}Please upgrade your Bash installation.${RESET}" + echo -e "${BOLD}${RED}If you've already upgraded via Homebrew, try running:${RESET}" + echo -e "${BOLD}${RED}/opt/homebrew/bin/bash $0 $@${RESET}" + exit 1 fi # Utility function to ensure all required tools are installed. check_required_tools() { - local tools=("curl" "jq" "chisel" "cast") - local missing_tools=() - - for tool in "${tools[@]}"; do - if ! command -v "$tool" &>/dev/null; then - missing_tools+=("$tool") - fi - done - - if [[ ${#missing_tools[@]} -ne 0 ]]; then - echo -e "${BOLD}${RED}The following required tools are not installed:${RESET}" - for tool in "${missing_tools[@]}"; do - echo -e "${BOLD}${RED} - $tool${RESET}" - done - echo -e "${BOLD}${RED}Please install them to run the script properly.${RESET}" - exit 1 - fi + local tools=("curl" "jq" "chisel" "cast") + local missing_tools=() + + for tool in "${tools[@]}"; do + if ! command -v "$tool" &>/dev/null; then + missing_tools+=("$tool") + fi + done + + if [[ ${#missing_tools[@]} -ne 0 ]]; then + echo -e "${BOLD}${RED}The following required tools are not installed:${RESET}" + for tool in "${missing_tools[@]}"; do + echo -e "${BOLD}${RED} - $tool${RESET}" + done + echo -e "${BOLD}${RED}Please install them to run the script properly.${RESET}" + exit 1 + fi } check_required_tools @@ -56,8 +56,8 @@ set -Eeuo pipefail # Enable debug mode if the environment variable `DEBUG` is set to `true`. if [[ "${DEBUG:-false}" == "true" ]]; then - # Print each command before executing it. - set -x + # Print each command before executing it. + set -x fi # Set the zero address as a global constant. @@ -65,7 +65,10 @@ readonly ZERO_ADDRESS="0x0000000000000000000000000000000000000000" # Set a global flag indicating whether a delegate call warning was already displayed # to the user. Used to determine proper spacing between multiple warnings. -delegate_call_warning_shown=false +delegate_call_warning_shown="false" + +# Set a global variable to store the Safe transaction hash for use across multiple functions. +global_safe_tx_hash="0x0000000000000000000000000000000000000000000000000000000000000000" # Set the type hash constants. # => `keccak256("EIP712Domain(uint256 chainId,address verifyingContract)");` @@ -91,11 +94,11 @@ readonly SAFE_MSG_TYPEHASH="0x60b3cbf8b4a223d68d641b3b6ddf9a298e7f33710cf3d3a9d1 # MultiSend `v1.3.0` (zksync): https://github.com/safe-global/safe-deployments/blob/4e25b09f62a4acec92b4ebe6b8ae496b3852d440/src/assets/v1.3.0/multi_send.json#L15, # Multisend `v1.4.1` (canonical): https://github.com/safe-global/safe-deployments/blob/4e25b09f62a4acec92b4ebe6b8ae496b3852d440/src/assets/v1.4.1/multi_send.json#L7. declare -a -r MultiSend=( - "0x8D29bE29923b68abfDD21e541b9374737B49cdAD" # MultiSend `v1.1.1` (canonical). - "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761" # MultiSend `v1.3.0` (canonical). - "0x998739BFdAAdde7C933B942a68053933098f9EDa" # MultiSend `v1.3.0` (eip155). - "0x0dFcccB95225ffB03c6FBB2559B530C2B7C8A912" # MultiSend `v1.3.0` (zksync). - "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526" # MultiSend `v1.4.1` (canonical). + "0x8D29bE29923b68abfDD21e541b9374737B49cdAD" # MultiSend `v1.1.1` (canonical). + "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761" # MultiSend `v1.3.0` (canonical). + "0x998739BFdAAdde7C933B942a68053933098f9EDa" # MultiSend `v1.3.0` (eip155). + "0x0dFcccB95225ffB03c6FBB2559B530C2B7C8A912" # MultiSend `v1.3.0` (zksync). + "0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526" # MultiSend `v1.4.1` (canonical). ) # Set the trusted (i.e. for delegate calls) `MultiSendCallOnly` addresses: @@ -104,22 +107,22 @@ declare -a -r MultiSend=( # MultiSendCallOnly `v1.3.0` (zksync): https://github.com/safe-global/safe-deployments/blob/4e25b09f62a4acec92b4ebe6b8ae496b3852d440/src/assets/v1.3.0/multi_send_call_only.json#L15, # MultiSendCallOnly `v1.4.1` (canonical): https://github.com/safe-global/safe-deployments/blob/4e25b09f62a4acec92b4ebe6b8ae496b3852d440/src/assets/v1.4.1/multi_send_call_only.json#L7. declare -a -r MultiSendCallOnly=( - "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D" # MultiSendCallOnly `v1.3.0` (canonical). - "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B" # MultiSendCallOnly `v1.3.0` (eip155). - "0xf220D3b4DFb23C4ade8C88E526C1353AbAcbC38F" # MultiSendCallOnly `v1.3.0` (zksync). - "0x9641d764fc13c8B624c04430C7356C1C7C8102e2" # MultiSendCallOnly `v1.4.1` (canonical). + "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D" # MultiSendCallOnly `v1.3.0` (canonical). + "0xA1dabEF33b3B82c7814B6D82A79e50F4AC44102B" # MultiSendCallOnly `v1.3.0` (eip155). + "0xf220D3b4DFb23C4ade8C88E526C1353AbAcbC38F" # MultiSendCallOnly `v1.3.0` (zksync). + "0x9641d764fc13c8B624c04430C7356C1C7C8102e2" # MultiSendCallOnly `v1.4.1` (canonical). ) # Set the trusted (i.e. for delegate calls) `SafeMigration` addresses: # SafeMigration `v1.4.1` (canonical): https://github.com/safe-global/safe-deployments/blob/4e25b09f62a4acec92b4ebe6b8ae496b3852d440/src/assets/v1.4.1/safe_migration.json#L7. declare -a -r SafeMigration=( - "0x526643F69b81B008F46d95CD5ced5eC0edFFDaC6" # SafeMigration `v1.4.1` (canonical). + "0x526643F69b81B008F46d95CD5ced5eC0edFFDaC6" # SafeMigration `v1.4.1` (canonical). ) # Set the trusted (i.e. for delegate calls) `SafeToL2Migration` addresses: # SafeToL2Migration `v1.4.1` (canonical): https://github.com/safe-global/safe-deployments/blob/4e25b09f62a4acec92b4ebe6b8ae496b3852d440/src/assets/v1.4.1/safe_to_l2_migration.json#L7. declare -a -r SafeToL2Migration=( - "0xfF83F6335d8930cBad1c0D439A841f01888D9f69" # SafeToL2Migration `v1.4.1` (canonical). + "0xfF83F6335d8930cBad1c0D439A841f01888D9f69" # SafeToL2Migration `v1.4.1` (canonical). ) # Set the trusted (i.e. for delegate calls) `SignMessageLib` addresses: @@ -128,89 +131,92 @@ declare -a -r SafeToL2Migration=( # SignMessageLib `v1.3.0` (zksync): https://github.com/safe-global/safe-deployments/blob/4e25b09f62a4acec92b4ebe6b8ae496b3852d440/src/assets/v1.3.0/sign_message_lib.json#L15, # SignMessageLib `v1.4.1` (canonical): https://github.com/safe-global/safe-deployments/blob/4e25b09f62a4acec92b4ebe6b8ae496b3852d440/src/assets/v1.4.1/sign_message_lib.json#L7. declare -a -r SignMessageLib=( - "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2" # SignMessageLib `v1.3.0` (canonical). - "0x98FFBBF51bb33A056B08ddf711f289936AafF717" # SignMessageLib `v1.3.0` (eip155). - "0x357147caf9C0cCa67DfA0CF5369318d8193c8407" # SignMessageLib `v1.3.0` (zksync). - "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9" # SignMessageLib `v1.4.1` (canonical). + "0xA65387F16B013cf2Af4605Ad8aA5ec25a2cbA3a2" # SignMessageLib `v1.3.0` (canonical). + "0x98FFBBF51bb33A056B08ddf711f289936AafF717" # SignMessageLib `v1.3.0` (eip155). + "0x357147caf9C0cCa67DfA0CF5369318d8193c8407" # SignMessageLib `v1.3.0` (zksync). + "0xd53cd0aB83D845Ac265BE939c57F53AD838012c9" # SignMessageLib `v1.4.1` (canonical). ) # Set the trusted (i.e. for delegate calls) contract addresses. # See: https://github.com/safe-global/safe-transaction-service/blob/c3b42f0bebff74b99fcdd958aee54b149e27eca5/safe_transaction_service/contracts/management/commands/setup_safe_contracts.py#L10-L16. declare -A -r TRUSTED_FOR_DELEGATE_CALL=( - ["MultiSend"]="${MultiSend[@]}" - ["MultiSendCallOnly"]="${MultiSendCallOnly[@]}" - ["SafeMigration"]="${SafeMigration[@]}" - ["SafeToL2Migration"]="${SafeToL2Migration[@]}" - ["SignMessageLib"]="${SignMessageLib[@]}" + ["MultiSend"]="${MultiSend[@]}" + ["MultiSendCallOnly"]="${MultiSendCallOnly[@]}" + ["SafeMigration"]="${SafeMigration[@]}" + ["SafeToL2Migration"]="${SafeToL2Migration[@]}" + ["SignMessageLib"]="${SignMessageLib[@]}" ) # Define the supported networks from the Safe transaction service. # See https://docs.safe.global/advanced/smart-account-supported-networks?service=Transaction+Service. declare -A -r API_URLS=( - ["arbitrum"]="https://safe-transaction-arbitrum.safe.global" - ["aurora"]="https://safe-transaction-aurora.safe.global" - ["avalanche"]="https://safe-transaction-avalanche.safe.global" - ["base"]="https://safe-transaction-base.safe.global" - ["base-sepolia"]="https://safe-transaction-base-sepolia.safe.global" - ["blast"]="https://safe-transaction-blast.safe.global" - ["bsc"]="https://safe-transaction-bsc.safe.global" - ["celo"]="https://safe-transaction-celo.safe.global" - ["ethereum"]="https://safe-transaction-mainnet.safe.global" - ["gnosis"]="https://safe-transaction-gnosis-chain.safe.global" - ["gnosis-chiado"]="https://safe-transaction-chiado.safe.global" - ["linea"]="https://safe-transaction-linea.safe.global" - ["mantle"]="https://safe-transaction-mantle.safe.global" - ["optimism"]="https://safe-transaction-optimism.safe.global" - ["polygon"]="https://safe-transaction-polygon.safe.global" - ["polygon-zkevm"]="https://safe-transaction-zkevm.safe.global" - ["scroll"]="https://safe-transaction-scroll.safe.global" - ["sepolia"]="https://safe-transaction-sepolia.safe.global" - ["worldchain"]="https://safe-transaction-worldchain.safe.global" - ["xlayer"]="https://safe-transaction-xlayer.safe.global" - ["zksync"]="https://safe-transaction-zksync.safe.global" + ["arbitrum"]="https://safe-transaction-arbitrum.safe.global" + ["aurora"]="https://safe-transaction-aurora.safe.global" + ["avalanche"]="https://safe-transaction-avalanche.safe.global" + ["base"]="https://safe-transaction-base.safe.global" + ["base-sepolia"]="https://safe-transaction-base-sepolia.safe.global" + ["blast"]="https://safe-transaction-blast.safe.global" + ["bsc"]="https://safe-transaction-bsc.safe.global" + ["celo"]="https://safe-transaction-celo.safe.global" + ["ethereum"]="https://safe-transaction-mainnet.safe.global" + ["gnosis"]="https://safe-transaction-gnosis-chain.safe.global" + ["gnosis-chiado"]="https://safe-transaction-chiado.safe.global" + ["linea"]="https://safe-transaction-linea.safe.global" + ["mantle"]="https://safe-transaction-mantle.safe.global" + ["optimism"]="https://safe-transaction-optimism.safe.global" + ["polygon"]="https://safe-transaction-polygon.safe.global" + ["polygon-zkevm"]="https://safe-transaction-zkevm.safe.global" + ["scroll"]="https://safe-transaction-scroll.safe.global" + ["sepolia"]="https://safe-transaction-sepolia.safe.global" + ["worldchain"]="https://safe-transaction-worldchain.safe.global" + ["xlayer"]="https://safe-transaction-xlayer.safe.global" + ["zksync"]="https://safe-transaction-zksync.safe.global" ) # Define the chain IDs of the supported networks from the Safe transaction service. declare -A -r CHAIN_IDS=( - ["arbitrum"]="42161" - ["aurora"]="1313161554" - ["avalanche"]="43114" - ["base"]="8453" - ["base-sepolia"]="84532" - ["blast"]="81457" - ["bsc"]="56" - ["celo"]="42220" - ["ethereum"]="1" - ["gnosis"]="100" - ["gnosis-chiado"]="10200" - ["linea"]="59144" - ["mantle"]="5000" - ["optimism"]="10" - ["polygon"]="137" - ["polygon-zkevm"]="1101" - ["scroll"]="534352" - ["sepolia"]="11155111" - ["worldchain"]="480" - ["xlayer"]="196" - ["zksync"]="324" + ["arbitrum"]="42161" + ["aurora"]="1313161554" + ["avalanche"]="43114" + ["base"]="8453" + ["base-sepolia"]="84532" + ["blast"]="81457" + ["bsc"]="56" + ["celo"]="42220" + ["ethereum"]="1" + ["gnosis"]="100" + ["gnosis-chiado"]="10200" + ["linea"]="59144" + ["mantle"]="5000" + ["optimism"]="10" + ["polygon"]="137" + ["polygon-zkevm"]="1101" + ["scroll"]="534352" + ["sepolia"]="11155111" + ["worldchain"]="480" + ["xlayer"]="196" + ["zksync"]="324" ) # Utility function to display the usage information. usage() { - cat <<EOF + cat <<EOF Usage: $0 [--help] [--version] [--list-networks] --network <network> --address <address> [--nonce <nonce>] + [--nested-safe-address <address>] [--nested-safe-nonce <nonce>] [--message <file>] [--interactive] Options: - --help Display this help message - --version Display the latest local commit hash (=version) of the script - --list-networks List all supported networks and their chain IDs - --network <network> Specify the network (required) - --address <address> Specify the Safe multisig address (required) - --nonce <nonce> Specify the transaction nonce (required for transaction hashes) - --message <file> Specify the message file (required for off-chain message hashes) - --interactive Use the interactive mode (optional for transaction hashes) + --help Display this help message + --version Display the latest local commit hash (=version) of the script + --list-networks List all supported networks and their chain IDs + --network <network> Specify the network (required) + --address <address> Specify the Safe multisig address (required) + --nonce <nonce> Specify the transaction nonce (required for transaction hashes) + --nested-safe-address <address> Specify the nested Safe multisig address (optional for transaction hashes or off-chain message hashes) + --nested-safe-nonce <nonce> Specify the nonce for the nested Safe transaction (optional for transaction hashes) + --message <file> Specify the message file (required for off-chain message hashes) + --interactive Use the interactive mode (optional for transaction hashes) Example for transaction hashes: $0 --network ethereum --address 0x1234...5678 --nonce 42 @@ -218,520 +224,702 @@ Example for transaction hashes: Example for transaction hashes (interactive mode): $0 --network ethereum --address 0x1234...5678 --nonce 42 --interactive +Example for transaction hashes via a nested Safe multisig approval: + $0 --network ethereum --address 0x1234...5678 --nonce 42 --nested-safe-address 0x8765...4321 --nested-safe-nonce 10 + +Example for transaction hashes via a nested Safe multisig approval (interactive mode): + $0 --network ethereum --address 0x1234...5678 --nonce 42 --nested-safe-address 0x8765...4321 --nested-safe-nonce 10 --interactive + Example for off-chain message hashes: $0 --network ethereum --address 0x1234...5678 --message message.txt + +Example for off-chain message hashes via a nested Safe multisig signer: + $0 --network ethereum --address 0x1234...5678 --nested-safe-address 0x8765...4321 --message message.txt EOF - exit "${1:-1}" + exit "${1:-1}" } # Utility function to retrieve the latest local commit hash from the Git repository. # We don't include `git` in the `check_required_tools` function to avoid making # it a strict dependency for the script to run. get_latest_git_commit_hash() { - local commit_hash="" - if command -v git &>/dev/null; then - commit_hash=$(git rev-parse HEAD 2>/dev/null) - if [[ -n "$commit_hash" ]]; then - echo -e "Latest local commit hash (=version) of the script: ${GREEN}$commit_hash${RESET}." - exit 0 - else - echo -e "${BOLD}${RED}No commit hash information available. There may be an issue with your Git installation or repository configuration.${RESET}" - exit 1 - fi - else - echo -e "${BOLD}${RED}Git is not installed or not found. Unable to retrieve the commit hash information!${RESET}" - exit 1 - fi + local commit_hash="" + if command -v git &>/dev/null; then + commit_hash=$(git rev-parse HEAD 2>/dev/null) + if [[ -n "$commit_hash" ]]; then + echo -e "Latest local commit hash (=version) of the script: ${GREEN}$commit_hash${RESET}." + exit 0 + else + echo -e "${BOLD}${RED}No commit hash information available. There may be an issue with your Git installation or repository configuration.${RESET}" + exit 1 + fi + else + echo -e "${BOLD}${RED}Git is not installed or not found. Unable to retrieve the commit hash information!${RESET}" + exit 1 + fi } # Utility function to list all supported networks. list_networks() { - echo "Supported Networks:" - for network in $(echo "${!CHAIN_IDS[@]}" | tr " " "\n" | sort); do - echo " $network (${CHAIN_IDS[$network]})" - done - exit 0 + echo "Supported Networks:" + for network in $(echo "${!CHAIN_IDS[@]}" | tr " " "\n" | sort); do + echo " $network (${CHAIN_IDS[$network]})" + done + exit 0 } # Utility function to print a section header. print_header() { - local header=$1 - if [[ -t 1 ]] && tput sgr0 >/dev/null 2>&1; then - # Terminal supports formatting. - printf "\n${UNDERLINE}%s${RESET}\n" "$header" - else - # Fallback for terminals without formatting support. - printf "\n%s\n" "> $header:" - fi + local header=$1 + if [[ -t 1 ]] && tput sgr0 >/dev/null 2>&1; then + # Terminal supports formatting. + printf "\n${UNDERLINE}%s${RESET}\n" "$header" + else + # Fallback for terminals without formatting support. + printf "\n%s\n" "> $header:" + fi } # Utility function to print a labelled value. print_field() { - local label=$1 - local value=$2 - local empty_line="${3:-false}" - - if [[ -t 1 ]] && tput sgr0 >/dev/null 2>&1; then - # Terminal supports formatting. - printf "%s: ${GREEN}%s${RESET}\n" "$label" "$value" - else - # Fallback for terminals without formatting support. - printf "%s: %s\n" "$label" "$value" - fi - - # Print an empty line if requested. - if [[ "$empty_line" == "true" ]]; then - printf "\n" - fi + local label=$1 + local value=$2 + local empty_line="${3:-false}" + + if [[ -t 1 ]] && tput sgr0 >/dev/null 2>&1; then + # Terminal supports formatting. + printf "%s: ${GREEN}%s${RESET}\n" "$label" "$value" + else + # Fallback for terminals without formatting support. + printf "%s: %s\n" "$label" "$value" + fi + + # Print an empty line if requested. + if [[ "$empty_line" == "true" ]]; then + printf "\n" + fi } # Utility function to print the transaction data. print_transaction_data() { - local address=$1 - local to=$2 - local value=$3 - local data=$4 - local operation=$5 - local safe_tx_gas=$6 - local base_gas=$7 - local gas_price=$8 - local gas_token=$9 - local refund_receiver=${10} - local nonce=${11} - local message=${12} - - print_header "Transaction Data" - print_field "Multisig address" "$address" - print_field "To" "$to" - print_field "Value" "$value" - print_field "Data" "$data" - case "$operation" in - 1) - if [[ "$operation" -eq 1 && ! " ${TRUSTED_FOR_DELEGATE_CALL[@]} " =~ " ${to} " ]]; then - print_field "Operation" "Delegatecall $(tput setaf 1)(UNTRUSTED delegatecall; carefully verify before proceeding!)$(tput sgr0)" - else - print_field "Operation" "Delegatecall $(tput setaf 3)(trusted delegatecall)$(tput sgr0)" - fi - ;; - 0) - print_field "Operation" "Call" - ;; - *) - print_field "Operation" "Unknown" - ;; - esac - print_field "Safe Transaction Gas" "$safe_tx_gas" - print_field "Base Gas" "$base_gas" - print_field "Gas Price" "$gas_price" - print_field "Gas Token" "$gas_token" - print_field "Refund Receiver" "$refund_receiver" - print_field "Nonce" "$nonce" - print_field "Encoded message" "$message" + local address=$1 + local to=$2 + local value=$3 + local data=$4 + local operation=$5 + local safe_tx_gas=$6 + local base_gas=$7 + local gas_price=$8 + local gas_token=$9 + local refund_receiver=${10} + local nonce=${11} + local message=${12} + + print_header "Transaction Data" + print_field "Multisig address" "$address" + print_field "To" "$to" + print_field "Value" "$value" + print_field "Data" "$data" + case "$operation" in + 1) + if [[ "$operation" -eq 1 && ! " ${TRUSTED_FOR_DELEGATE_CALL[@]} " =~ " ${to} " ]]; then + print_field "Operation" "Delegatecall $(tput setaf 1)(UNTRUSTED delegatecall; carefully verify before proceeding!)$(tput sgr0)" + else + print_field "Operation" "Delegatecall $(tput setaf 3)(trusted delegatecall)$(tput sgr0)" + fi + ;; + 0) + print_field "Operation" "Call" + ;; + *) + print_field "Operation" "Unknown" + ;; + esac + print_field "Safe Transaction Gas" "$safe_tx_gas" + print_field "Base Gas" "$base_gas" + print_field "Gas Price" "$gas_price" + print_field "Gas Token" "$gas_token" + print_field "Refund Receiver" "$refund_receiver" + print_field "Nonce" "$nonce" + print_field "Encoded message" "$message" } # Utility function to format the hash (keep `0x` lowercase, rest uppercase). format_hash() { - local hash=$1 - local prefix="${hash:0:2}" - local rest="${hash:2}" - echo "${prefix,,}${rest^^}" + local hash=$1 + local prefix="${hash:0:2}" + local rest="${hash:2}" + echo "${prefix,,}${rest^^}" } # Utility function to print the hash information. print_hash_info() { - local domain_hash=$1 - local message_hash=$2 - local safe_tx_hash=$3 - - print_header "Hashes" - print_field "Domain hash" "$(format_hash "$domain_hash")" - print_field "Message hash" "$(format_hash "$message_hash")" - print_field "Safe transaction hash" "$safe_tx_hash" + local domain_hash=$1 + local message_hash=$2 + local safe_tx_hash=$3 + + print_header "Hashes" + print_field "Domain hash" "$(format_hash "$domain_hash")" + print_field "Message hash" "$(format_hash "$message_hash")" + print_field "Safe transaction hash" "$safe_tx_hash" } # Utility function to print the ABI-decoded transaction data. print_decoded_data() { - local data=$1 - local data_decoded=$2 - - if [[ "$data" == "0x" && "$data_decoded" == "0x" ]]; then - print_field "Method" "0x (ETH Transfer)" - print_field "Parameters" "[]" - elif [[ "$data" != "0x" && "$data_decoded" == "0x" ]]; then - print_field "Method" "Unknown" - print_field "Parameters" "Unknown" - elif [[ "$data_decoded" == "interactive" ]]; then - print_field "Method" "Unavailable in interactive mode" - print_field "Parameters" "Unavailable in interactive mode" - else - local method=$(echo "$data_decoded" | jq -r ".method") - local parameters=$(echo "$data_decoded" | jq -r ".parameters") - - print_field "Method" "$method" - print_field "Parameters" "$parameters" - - # Check if the called function is sensitive and print a warning in bold. - case "$method" in - addOwnerWithThreshold | removeOwner | swapOwner | changeThreshold) - echo - echo -e "${BOLD}${RED}WARNING: The \"$method\" function modifies the owners or threshold of the Safe. Proceed with caution!${RESET}" - ;; - esac - - # Check for sensitive functions in nested transactions. - echo "$parameters" | jq -c ".[] | .valueDecoded[]? | select(.dataDecoded != null)" | while read -r nested_param; do - nested_method=$(echo "$nested_param" | jq -r ".dataDecoded.method") - - if [[ "$nested_method" =~ ^(addOwnerWithThreshold|removeOwner|swapOwner|changeThreshold)$ ]]; then - echo - echo -e "${BOLD}${RED}WARNING: The \"$nested_method\" function modifies the owners or threshold of the Safe! Proceed with caution!${RESET}" - fi - done - fi + local data=$1 + local data_decoded=$2 + + if [[ "$data" == "0x" && "$data_decoded" == "0x" ]]; then + print_field "Method" "0x (ETH Transfer)" + print_field "Parameters" "[]" + elif [[ "$data" != "0x" && "$data_decoded" == "0x" ]]; then + print_field "Method" "Unknown" + print_field "Parameters" "Unknown" + elif [[ "$data_decoded" == "interactive" ]]; then + print_field "Method" "Unavailable in interactive mode" + print_field "Parameters" "Unavailable in interactive mode" + else + local method=$(echo "$data_decoded" | jq -r ".method") + local parameters=$(echo "$data_decoded" | jq -r ".parameters") + + print_field "Method" "$method" + print_field "Parameters" "$parameters" + + # Check if the called function is sensitive and print a warning in bold. + case "$method" in + addOwnerWithThreshold | removeOwner | swapOwner | changeThreshold) + echo + echo -e "${BOLD}${RED}WARNING: The \"$method\" function modifies the owners or threshold of the Safe. Proceed with caution!${RESET}" + ;; + esac + + # Check for sensitive functions in nested transactions. + echo "$parameters" | jq -c ".[] | .valueDecoded[]? | select(.dataDecoded != null)" | while read -r nested_param; do + nested_method=$(echo "$nested_param" | jq -r ".dataDecoded.method") + + if [[ "$nested_method" =~ ^(addOwnerWithThreshold|removeOwner|swapOwner|changeThreshold)$ ]]; then + echo + echo -e "${BOLD}${RED}WARNING: The \"$nested_method\" function modifies the owners or threshold of the Safe! Proceed with caution!${RESET}" + fi + done + fi } # Utility function to extract the clean Safe multisig version. get_version() { - local version=$1 - # Safe multisig versions can have the format `X.Y.Z+L2`. - # Remove any suffix after and including the `+` in the version string for comparison. - local clean_version=$(echo "$version" | sed "s/+.*//") - echo "$clean_version" + local version=$1 + # Safe multisig versions can have the format `X.Y.Z+L2`. + # Remove any suffix after and including the `+` in the version string for comparison. + local clean_version=$(echo "$version" | sed "s/+.*//") + echo "$clean_version" } # Utility function to validate the Safe multisig version. validate_version() { - local version=$1 - if [[ -z "$version" ]]; then - echo "$(tput setaf 3)No Safe multisig contract found for the specified network. Please ensure that you have selected the correct network.$(tput sgr0)" - exit 0 - fi - - local clean_version=$(get_version "$version") - - # Ensure that the Safe multisig version is `>= 0.1.0`. - if [[ "$(printf "%s\n%s" "$clean_version" "0.1.0" | sort -V | head -n1)" == "$clean_version" && "$clean_version" != "0.1.0" ]]; then - echo "$(tput setaf 3)Safe multisig version \"${clean_version}\" is not supported!$(tput sgr0)" - exit 0 - fi + local version=$1 + if [[ -z "$version" ]]; then + echo "$(tput setaf 3)No Safe multisig contract found for the specified network. Please ensure that you have selected the correct network.$(tput sgr0)" + exit 0 + fi + + local clean_version=$(get_version "$version") + + # Ensure that the Safe multisig version is `>= 0.1.0`. + if [[ "$(printf "%s\n%s" "$clean_version" "0.1.0" | sort -V | head -n1)" == "$clean_version" && "$clean_version" != "0.1.0" ]]; then + echo "$(tput setaf 3)Safe multisig version \"${clean_version}\" is not supported!$(tput sgr0)" + exit 0 + fi } # Utility function to calculate the domain hash. calculate_domain_hash() { - local version=$1 - local domain_separator_typehash=$2 - local domain_hash_args=$3 - - # Validate the Safe multisig version. - validate_version "$version" - - local clean_version=$(get_version "$version") - - # Safe multisig versions `<= 1.2.0` use a legacy (i.e. without `chainId`) `DOMAIN_SEPARATOR_TYPEHASH` value. - # Starting with version `1.3.0`, the `chainId` field was introduced: https://github.com/safe-global/safe-smart-account/pull/264. - if [[ "$(printf "%s\n%s" "$clean_version" "1.2.0" | sort -V | head -n1)" == "$clean_version" ]]; then - domain_separator_typehash="$DOMAIN_SEPARATOR_TYPEHASH_OLD" - domain_hash_args="$domain_separator_typehash, $address" - fi - - # Calculate the domain hash. - local domain_hash=$(chisel eval "keccak256(abi.encode($domain_hash_args))" | - awk '/Data:/ {gsub(/\x1b\[[0-9;]*m/, "", $3); print $3}') - echo "$domain_hash" + local version=$1 + local domain_separator_typehash=$2 + local domain_hash_args=$3 + + # Validate the Safe multisig version. + validate_version "$version" + + local clean_version=$(get_version "$version") + + # Safe multisig versions `<= 1.2.0` use a legacy (i.e. without `chainId`) `DOMAIN_SEPARATOR_TYPEHASH` value. + # Starting with version `1.3.0`, the `chainId` field was introduced: https://github.com/safe-global/safe-smart-account/pull/264. + if [[ "$(printf "%s\n%s" "$clean_version" "1.2.0" | sort -V | head -n1)" == "$clean_version" ]]; then + domain_separator_typehash="$DOMAIN_SEPARATOR_TYPEHASH_OLD" + domain_hash_args="$domain_separator_typehash, $address" + fi + + # Calculate the domain hash. + local domain_hash=$(chisel eval "keccak256(abi.encode($domain_hash_args))" | + awk '/Data:/ {gsub(/\x1b\[[0-9;]*m/, "", $3); print $3}') + echo "$domain_hash" } # Utility function to calculate the domain and message hashes. calculate_hashes() { - local chain_id=$1 - local address=$2 - local to=$3 - local value=$4 - local data=$5 - local operation=$6 - local safe_tx_gas=$7 - local base_gas=$8 - local gas_price=$9 - local gas_token=${10} - local refund_receiver=${11} - local nonce=${12} - local data_decoded=${13} - local version=${14} - - local domain_separator_typehash="$DOMAIN_SEPARATOR_TYPEHASH" - local domain_hash_args="$domain_separator_typehash, $chain_id, $address" - local safe_tx_typehash="$SAFE_TX_TYPEHASH" - - # Validate the Safe multisig version. - validate_version "$version" - - local clean_version=$(get_version "$version") - - # Calculate the domain hash. - local domain_hash=$(calculate_domain_hash "$version" "$domain_separator_typehash" "$domain_hash_args") - - # Calculate the data hash. - # The dynamic value `bytes` is encoded as a `keccak256` hash of its content. - # See: https://eips.ethereum.org/EIPS/eip-712#definition-of-encodedata. - local data_hashed=$(cast keccak "$data") - - # Safe multisig versions `< 1.0.0` use a legacy (i.e. the parameter value `baseGas` was - # called `dataGas` previously) `SAFE_TX_TYPEHASH` value. Starting with version `1.0.0`, - # `baseGas` was introduced: https://github.com/safe-global/safe-smart-account/pull/90. - if [[ "$(printf "%s\n%s" "$clean_version" "1.0.0" | sort -V | head -n1)" == "$clean_version" && "$clean_version" != "1.0.0" ]]; then - safe_tx_typehash="$SAFE_TX_TYPEHASH_OLD" - fi - - # Encode the message. - local message=$(cast abi-encode "SafeTxStruct(bytes32,address,uint256,bytes32,uint8,uint256,uint256,uint256,address,address,uint256)" \ - "$safe_tx_typehash" \ - "$to" \ - "$value" \ - "$data_hashed" \ - "$operation" \ - "$safe_tx_gas" \ - "$base_gas" \ - "$gas_price" \ - "$gas_token" \ - "$refund_receiver" \ - "$nonce") - - # Calculate the message hash. - local message_hash=$(cast keccak "$message") - - # Calculate the Safe transaction hash. - local safe_tx_hash=$(chisel eval "keccak256(abi.encodePacked(bytes1(0x19), bytes1(0x01), bytes32($domain_hash), bytes32($message_hash)))" | - awk '/Data:/ {gsub(/\x1b\[[0-9;]*m/, "", $3); print $3}') - - # Print the retrieved transaction data. - print_transaction_data "$address" "$to" "$value" "$data" "$operation" "$safe_tx_gas" "$base_gas" "$gas_price" "$gas_token" "$refund_receiver" "$nonce" "$message" - # Print the ABI-decoded transaction data. - print_decoded_data "$data" "$data_decoded" - # Print the results with the same formatting for "Domain hash" and "Message hash" as a Ledger hardware device. - print_hash_info "$domain_hash" "$message_hash" "$safe_tx_hash" + local chain_id=$1 + local address=$2 + local to=$3 + local value=$4 + local data=$5 + local operation=$6 + local safe_tx_gas=$7 + local base_gas=$8 + local gas_price=$9 + local gas_token=${10} + local refund_receiver=${11} + local nonce=${12} + local data_decoded=${13} + local version=${14} + local update_global_safe_tx_hash=${15:-} + + local domain_separator_typehash="$DOMAIN_SEPARATOR_TYPEHASH" + local domain_hash_args="$domain_separator_typehash, $chain_id, $address" + local safe_tx_typehash="$SAFE_TX_TYPEHASH" + + # Validate the Safe multisig version. + validate_version "$version" + + local clean_version=$(get_version "$version") + + # Calculate the domain hash. + local domain_hash=$(calculate_domain_hash "$version" "$domain_separator_typehash" "$domain_hash_args") + + # Calculate the data hash. + # The dynamic value `bytes` is encoded as a `keccak256` hash of its content. + # See: https://eips.ethereum.org/EIPS/eip-712#definition-of-encodedata. + local data_hashed=$(cast keccak "$data") + + # Safe multisig versions `< 1.0.0` use a legacy (i.e. the parameter value `baseGas` was + # called `dataGas` previously) `SAFE_TX_TYPEHASH` value. Starting with version `1.0.0`, + # `baseGas` was introduced: https://github.com/safe-global/safe-smart-account/pull/90. + if [[ "$(printf "%s\n%s" "$clean_version" "1.0.0" | sort -V | head -n1)" == "$clean_version" && "$clean_version" != "1.0.0" ]]; then + safe_tx_typehash="$SAFE_TX_TYPEHASH_OLD" + fi + + # Encode the message. + local message=$(cast abi-encode "SafeTxStruct(bytes32,address,uint256,bytes32,uint8,uint256,uint256,uint256,address,address,uint256)" \ + "$safe_tx_typehash" \ + "$to" \ + "$value" \ + "$data_hashed" \ + "$operation" \ + "$safe_tx_gas" \ + "$base_gas" \ + "$gas_price" \ + "$gas_token" \ + "$refund_receiver" \ + "$nonce") + + # Calculate the message hash. + local message_hash=$(cast keccak "$message") + + # Calculate the Safe transaction hash. + local safe_tx_hash=$(chisel eval "keccak256(abi.encodePacked(bytes1(0x19), bytes1(0x01), bytes32($domain_hash), bytes32($message_hash)))" | + awk '/Data:/ {gsub(/\x1b\[[0-9;]*m/, "", $3); print $3}') + + # Print the retrieved transaction data. + print_transaction_data "$address" "$to" "$value" "$data" "$operation" "$safe_tx_gas" "$base_gas" "$gas_price" "$gas_token" "$refund_receiver" "$nonce" "$message" + # Print the ABI-decoded transaction data. + print_decoded_data "$data" "$data_decoded" + # Print the results with the same formatting for "Domain hash" and "Message hash" as a Ledger hardware device. + print_hash_info "$domain_hash" "$message_hash" "$safe_tx_hash" + + # Store the Safe transaction hash so it can be captured by a calling function. + if [[ -n "$update_global_safe_tx_hash" ]]; then + global_safe_tx_hash="$safe_tx_hash" + readonly global_safe_tx_hash + fi +} + +# Utility function to calculate the domain and message hashes for a nested Safe multisig address. +calculate_nested_safe_hashes() { + local chain_id=$1 + local target_safe_address=$2 + local nested_safe_address=$3 + local nested_safe_nonce=$4 + local safe_tx_hash=$5 + local nested_safe_version=$6 + + # Set the fixed parameters for the `approveHash` transaction. + local to="$target_safe_address" + local value="0" + # Encode the `approveHash(bytes32)` function call with the Safe transaction hash. + # See (`approveHash` function): https://github.com/safe-global/safe-smart-account/blob/bdcfce3a76c4d1dfb256ac2ca971be7cfd6e493a/contracts/Safe.sol#L372-L379. + # See (`execTransaction` function part): https://github.com/safe-global/safe-smart-account/blob/bdcfce3a76c4d1dfb256ac2ca971be7cfd6e493a/contracts/Safe.sol#L108-L143. + # See (`checkNSignatures` function part): https://github.com/safe-global/safe-smart-account/blob/bdcfce3a76c4d1dfb256ac2ca971be7cfd6e493a/contracts/Safe.sol#L318-L323. + # => `bytes4(keccak256("approveHash(bytes32)"));` + local approve_hash_signature="0xd4d9bdcd" + # => `abi.encodePacked(bytes4(keccak256("approveHash(bytes32)")), bytes32(safeTxHash));` + local data="${approve_hash_signature}${safe_tx_hash#0x}" + local operation="0" + local safe_tx_gas="0" + local base_gas="0" + local gas_price="0" + local gas_token="$ZERO_ADDRESS" + local refund_receiver="$ZERO_ADDRESS" + local nonce="$nested_safe_nonce" + local data_decoded="{\"method\": \"approveHash\", \"parameters\": [{\"name\": \"hashToApprove\", \"type\": \"bytes32\", \"value\": \"$safe_tx_hash\"}]}" + + echo -e "\n${BOLD}${UNDERLINE}Nested Safe \`approveHash\` Transaction Data and Computed Hashes${RESET}" + cat <<EOF + +$(tput setaf 3)The specified nested Safe at $nested_safe_address will use the following transaction to approve the primary transaction.$(tput sgr0) +EOF + + # Calculate the domain and message hashes for the specified nested Safe multisig address. + calculate_hashes "$chain_id" \ + "$nested_safe_address" \ + "$to" \ + "$value" \ + "$data" \ + "$operation" \ + "$safe_tx_gas" \ + "$base_gas" \ + "$gas_price" \ + "$gas_token" \ + "$refund_receiver" \ + "$nonce" \ + "$data_decoded" \ + "$nested_safe_version" } # Utility function to validate the network name. validate_network() { - local network="$1" - - if [[ -z "$network" ]]; then - echo -e "${BOLD}${RED}Network name is empty!${RESET}" >&2 - echo - calculate_safe_hashes --list-networks >&2 - exit 1 - fi - - if [[ -z "${API_URLS[$network]:-}" || -z "${CHAIN_IDS[$network]:-}" ]]; then - echo -e "${BOLD}${RED}Invalid network name: \"${network}\"${RESET}\n" >&2 - echo - calculate_safe_hashes --list-networks >&2 - exit 1 - fi + local network="$1" + + if [[ -z "$network" ]]; then + echo -e "${BOLD}${RED}Network name is empty!${RESET}" >&2 + echo + calculate_safe_hashes --list-networks >&2 + exit 1 + fi + + if [[ -z "${API_URLS[$network]:-}" || -z "${CHAIN_IDS[$network]:-}" ]]; then + echo -e "${BOLD}${RED}Invalid network name: \"${network}\"${RESET}\n" >&2 + echo + calculate_safe_hashes --list-networks >&2 + exit 1 + fi } # Utility function to retrieve the API URL of the selected network. get_api_url() { - local network="$1" - validate_network "$network" - echo "${API_URLS[$network]}" + local network="$1" + validate_network "$network" + echo "${API_URLS[$network]}" } # Utility function to retrieve the chain ID of the selected network. get_chain_id() { - local network="$1" - validate_network "$network" - echo "${CHAIN_IDS[$network]}" + local network="$1" + validate_network "$network" + echo "${CHAIN_IDS[$network]}" } # Utility function to validate the multisig address. validate_address() { - local address="$1" - if [[ -z "$address" || ! "$address" =~ ^0x[a-fA-F0-9]{40}$ ]]; then - echo -e "${BOLD}${RED}Invalid Ethereum address format: \"${address}\"${RESET}" >&2 - exit 1 - fi + local address="$1" + if [[ -z "$address" || ! "$address" =~ ^0x[a-fA-F0-9]{40}$ ]]; then + echo -e "${BOLD}${RED}Invalid Ethereum address format: \"${address}\"${RESET}" >&2 + exit 1 + fi } # Utility function to validate a value parameter. validate_value() { - local value="$1" - local name="$2" - if [[ -z "$value" || ! "$value" =~ ^[0-9]+$ ]]; then - echo -e "${BOLD}${RED}Invalid \`${name}\` value: \"${value}\". Must be a non-negative integer!${RESET}" >&2 - exit 1 - fi + local value="$1" + local name="$2" + if [[ -z "$value" || ! "$value" =~ ^[0-9]+$ ]]; then + echo -e "${BOLD}${RED}Invalid \`${name}\` value: \"${value}\". Must be a non-negative integer!${RESET}" >&2 + exit 1 + fi } # Utility function to warn the user if the transaction includes an untrusted delegate call. warn_if_delegate_call() { - local operation="$1" - local to="$2" + local operation="$1" + local to="$2" - # Warn the user if `operation` equals `1`, implying a `delegatecall`, and if the `to` address is untrusted. - # See: https://github.com/safe-global/safe-smart-account/blob/34359e8305d618b7d74e39ed370a6b59ab14f827/contracts/libraries/Enum.sol. - if [[ "$operation" -eq 1 && ! " ${TRUSTED_FOR_DELEGATE_CALL[@]} " =~ " ${to} " ]]; then - cat <<EOF + # Warn the user if `operation` equals `1`, implying a `delegatecall`, and if the `to` address is untrusted. + # See: https://github.com/safe-global/safe-smart-account/blob/34359e8305d618b7d74e39ed370a6b59ab14f827/contracts/libraries/Enum.sol. + if [[ "$operation" -eq 1 && ! " ${TRUSTED_FOR_DELEGATE_CALL[@]} " =~ " ${to} " ]]; then + cat <<EOF $(tput setaf 1)WARNING: The transaction includes an untrusted delegate call to address $to! This may lead to unexpected behaviour or vulnerabilities. Please review it carefully before you sign!$(tput sgr0) EOF - delegate_call_warning_shown=true - readonly delegate_call_warning_shown - fi + delegate_call_warning_shown="true" + readonly delegate_call_warning_shown + fi } # Utility function to check for a potential gas token attack. check_gas_token_attack() { - local gas_price=$1 - local gas_token=$2 - local refund_receiver=$3 - local warning_message="" + local gas_price=$1 + local gas_token=$2 + local refund_receiver=$3 + local warning_message="" - if [[ "$gas_token" != "$ZERO_ADDRESS" && "$refund_receiver" != "$ZERO_ADDRESS" ]]; then - warning_message+="$(tput setaf 1)WARNING: This transaction uses a custom gas token and a custom refund receiver. + if [[ "$gas_token" != "$ZERO_ADDRESS" && "$refund_receiver" != "$ZERO_ADDRESS" ]]; then + warning_message+="$(tput setaf 1)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.$(tput sgr0)\n" - if [[ "$gas_price" != "0" ]]; then - warning_message+="$(tput setaf 1)Furthermore, the gas price is non-zero, which increases the potential for hidden value transfers.$(tput sgr0)\n" - fi - elif [[ "$gas_token" != "$ZERO_ADDRESS" ]]; then - warning_message+="$(tput setaf 3)WARNING: This transaction uses a custom gas token. Please verify that this is intended.$(tput sgr0)\n" - elif [[ "$refund_receiver" != "$ZERO_ADDRESS" ]]; then - warning_message+="$(tput setaf 3)WARNING: This transaction uses a custom refund receiver. Please verify that this is intended.$(tput sgr0)\n" - fi - - if [[ -n "$warning_message" ]]; then - if [[ "$delegate_call_warning_shown" != "true" ]]; then - echo -e "\n$warning_message" - else - echo -e "$warning_message" - fi - fi + if [[ "$gas_price" != "0" ]]; then + warning_message+="$(tput setaf 1)Furthermore, the gas price is non-zero, which increases the potential for hidden value transfers.$(tput sgr0)\n" + fi + elif [[ "$gas_token" != "$ZERO_ADDRESS" ]]; then + warning_message+="$(tput setaf 3)WARNING: This transaction uses a custom gas token. Please verify that this is intended.$(tput sgr0)\n" + elif [[ "$refund_receiver" != "$ZERO_ADDRESS" ]]; then + warning_message+="$(tput setaf 3)WARNING: This transaction uses a custom refund receiver. Please verify that this is intended.$(tput sgr0)\n" + fi + + if [[ -n "$warning_message" ]]; then + if [[ "$delegate_call_warning_shown" != "true" ]]; then + echo -e "\n$warning_message" + else + echo -e "$warning_message" + fi + fi } # Utility function to validate the message file. validate_message_file() { - local message_file="$1" - if [[ ! -f "$message_file" ]]; then - echo -e "${BOLD}${RED}Message file not found: \"${message_file}\"!${RESET}" >&2 - exit 1 - fi - if [[ ! -s "$message_file" ]]; then - echo -e "${BOLD}${RED}Message file is empty: \"${message_file}\"!${RESET}" >&2 - exit 1 - fi + local message_file="$1" + if [[ ! -f "$message_file" ]]; then + echo -e "${BOLD}${RED}Message file not found: \"${message_file}\"!${RESET}" >&2 + exit 1 + fi + if [[ ! -s "$message_file" ]]; then + echo -e "${BOLD}${RED}Message file is empty: \"${message_file}\"!${RESET}" >&2 + exit 1 + fi } # Utility function to calculate the domain and message hashes for off-chain messages. calculate_offchain_message_hashes() { - local network=$1 - local chain_id=$2 - local address=$3 - local message_file=$4 - local version=$5 - - validate_message_file "$message_file" - - # Validate the Safe multisig version. - validate_version "$version" - - local message_raw=$(< "$message_file") - # Normalise line endings to `LF` (`\n`). - message_raw=$(echo "$message_raw" | tr -d "\r") - local hashed_message=$(cast hash-message "$message_raw") - - local domain_separator_typehash="$DOMAIN_SEPARATOR_TYPEHASH" - local domain_hash_args="$domain_separator_typehash, $chain_id, $address" - - # Calculate the domain hash. - local domain_hash=$(calculate_domain_hash "$version" "$domain_separator_typehash" "$domain_hash_args") - - # Calculate the message hash. - local message_hash=$(chisel eval "keccak256(abi.encode(bytes32($SAFE_MSG_TYPEHASH), keccak256(abi.encode(bytes32($hashed_message)))))" | - awk '/Data:/ {gsub(/\x1b\[[0-9;]*m/, "", $3); print $3}') - - # Calculate the Safe message hash. - local safe_msg_hash=$(chisel eval "keccak256(abi.encodePacked(bytes1(0x19), bytes1(0x01), bytes32($domain_hash), bytes32($message_hash)))" | - awk '/Data:/ {gsub(/\x1b\[[0-9;]*m/, "", $3); print $3}') - - # Calculate and display the hashes. - echo "===================================" - echo "= Selected Network Configurations =" - echo -e "===================================\n" - print_field "Network" "$network" - print_field "Chain ID" "$chain_id" true - echo "====================================" - echo "= Message Data and Computed Hashes =" - echo "====================================" - print_header "Message Data" - print_field "Multisig address" "$address" - print_field "Message" "$message_raw" - print_header "Hashes" - print_field "Raw message hash" "$hashed_message" - print_field "Domain hash" "$(format_hash "$domain_hash")" - print_field "Message hash" "$(format_hash "$message_hash")" - print_field "Safe message hash" "$safe_msg_hash" + local network=$1 + local chain_id=$2 + local address=$3 + local message_file=$4 + local version=$5 + + validate_message_file "$message_file" + + # Validate the Safe multisig version. + validate_version "$version" + + local message_raw=$(<"$message_file") + # Normalise line endings to `LF` (`\n`). + message_raw=$(echo "$message_raw" | tr -d "\r") + local hashed_message=$(cast hash-message "$message_raw") + + local domain_separator_typehash="$DOMAIN_SEPARATOR_TYPEHASH" + local domain_hash_args="$domain_separator_typehash, $chain_id, $address" + + # Calculate the domain hash. + local domain_hash=$(calculate_domain_hash "$version" "$domain_separator_typehash" "$domain_hash_args") + + # Calculate the message hash. + local message_hash=$(chisel eval "keccak256(abi.encode(bytes32($SAFE_MSG_TYPEHASH), keccak256(abi.encode(bytes32($hashed_message)))))" | + awk '/Data:/ {gsub(/\x1b\[[0-9;]*m/, "", $3); print $3}') + + # Calculate the Safe message hash. + local safe_msg_hash=$(chisel eval "keccak256(abi.encodePacked(bytes1(0x19), bytes1(0x01), bytes32($domain_hash), bytes32($message_hash)))" | + awk '/Data:/ {gsub(/\x1b\[[0-9;]*m/, "", $3); print $3}') + + # Calculate and display the hashes. + echo "===================================" + echo "= Selected Network Configurations =" + echo -e "===================================\n" + print_field "Network" "$network" + print_field "Chain ID" "$chain_id" true + echo "====================================" + echo "= Message Data and Computed Hashes =" + echo "====================================" + print_header "Message Data" + print_field "Multisig address" "$address" + print_field "Message" "$message_raw" + print_header "Hashes" + print_field "Safe message" "$hashed_message" + print_field "Domain hash" "$(format_hash "$domain_hash")" + print_field "Message hash" "$(format_hash "$message_hash")" + print_field "Safe message hash" "$safe_msg_hash" +} + +# Utility function to calculate the domain and message hashes for off-chain messages, +# using a nested Safe multisig address. Please note that off-chain messages are hashed +# and passed to a nested Safe via EIP-712 (https://eips.ethereum.org/EIPS/eip-712) objects. +calculate_nested_safe_offchain_message_hashes() { + local chain_id=$1 + local address=$2 + local nested_safe_address=$3 + local message_file=$4 + local nested_safe_version=$5 + + validate_message_file "$message_file" + + # Validate the Safe multisig version. + validate_version "$nested_safe_version" + + local message_raw=$(<"$message_file") + # Normalise line endings to `LF` (`\n`). + message_raw=$(echo "$message_raw" | tr -d "\r") + local hashed_message=$(cast hash-message "$message_raw") + + local domain_separator_typehash="$DOMAIN_SEPARATOR_TYPEHASH" + local domain_hash_args="$domain_separator_typehash, $chain_id, $nested_safe_address" + + # Calculate the Safe multisig domain hash. + local safe_domain_hash=$(calculate_domain_hash "$nested_safe_version" "$domain_separator_typehash" "$domain_hash_args") + + # Calculate the EIP-712 message domain hash. + local message_domain_hash=$(chisel eval "keccak256(abi.encode(bytes32($domain_separator_typehash), uint256($chain_id), address($address)))" | + awk '/Data:/ {gsub(/\x1b\[[0-9;]*m/, "", $3); print $3}') + + # Encode the message. Please note the value `hashed_message` is treated as a string and encoded + # as a `keccak256` hash of its content according to EIP-712 (https://eips.ethereum.org/EIPS/eip-712#definition-of-encodedata). + local message=$(cast abi-encode "SafeMessage(bytes32,bytes32)" "$SAFE_MSG_TYPEHASH" "$(cast keccak "$hashed_message")") + + # Hash the message. + local hashed_encoded_message=$(cast keccak "$message") + + # Calculate the Safe message. + local safe_msg=$(chisel eval "keccak256(abi.encodePacked(bytes1(0x19), bytes1(0x01), bytes32($message_domain_hash), bytes32($hashed_encoded_message)))" | + awk '/Data:/ {gsub(/\x1b\[[0-9;]*m/, "", $3); print $3}') + + # Calculate the message hash. + local message_hash=$(chisel eval "keccak256(abi.encode(bytes32($SAFE_MSG_TYPEHASH), keccak256(abi.encode(bytes32($safe_msg)))))" | + awk '/Data:/ {gsub(/\x1b\[[0-9;]*m/, "", $3); print $3}') + + # Calculate the Safe message hash. + local safe_msg_hash=$(chisel eval "keccak256(abi.encodePacked(bytes1(0x19), bytes1(0x01), bytes32($safe_domain_hash), bytes32($message_hash)))" | + awk '/Data:/ {gsub(/\x1b\[[0-9;]*m/, "", $3); print $3}') + + echo -e "\n${BOLD}${UNDERLINE}Nested Safe Computed Hashes${RESET}" + + cat <<EOF + +$(tput setaf 3)The specified nested Safe at $nested_safe_address will sign the above displayed Safe message $hashed_message via an EIP-712 message object.$(tput sgr0) +EOF + + # Calculate and display the hashes. + print_header "Hashes" + print_field "Safe message" "$safe_msg" + print_field "Domain hash" "$(format_hash "$safe_domain_hash")" + print_field "Message hash" "$(format_hash "$message_hash")" + print_field "Safe message hash" "$safe_msg_hash" } -# Safe Transaction/Message Hashes Calculator +############################################## +# Safe Transaction/Message Hashes Calculator # +############################################## # This function orchestrates the entire process of calculating the Safe transaction/message hashes: -# 1. Parses command-line arguments (`help`, `version`, `list-networks`, `network`, `address`, `nonce`, `message`, `interactive`). +# 1. Parses command-line arguments (`help`, `version`, `list-networks`, `network`, `address`, `nonce`, +# `nested-safe-address`, `nested-safe-nonce`, `message`, `interactive`). # 2. Validates that all required parameters are provided. # 3. Retrieves the API URL and chain ID for the specified network. # 4. Constructs the API endpoint URL. # 5. If a message file is provided: # - Validates that no interactive mode is specified (as it's not applicable for off-chain message hashes). -# - Validates that no nonce is specified (as it's not applicable for off-chain message hashes). +# - Validates that no `nonce` or `nested-safe-nonce` is specified (as it's not applicable for off-chain message hashes). # - Calls `calculate_offchain_message_hashes` to compute and display the message hashes. -# 6. If a nonce is provided: +# - If a nested Safe address is provided, invokes the `calculate_nested_safe_offchain_message_hashes` function with the nested +# parameter values and displays the resulting hashes. +# 6. If a `nonce` is provided: # - Fetches the transaction data from the Safe transaction service API. # - Extracts the relevant transaction details from the API response. # - If the interactive mode is specified, overrides the desired parameter values. # - Warns the user if the transaction includes an untrusted delegate call. # - Checks for a potential gas token attack. # - Calls the `calculate_hashes` function to compute and display the results. +# - If nested Safe parameters are provided, invokes the `calculate_nested_safe_hashes` function with the approval transaction +# data and displays the resulting hashes. calculate_safe_hashes() { - # Display the help message if no arguments are provided. - if [[ $# -eq 0 ]]; then - usage - fi - - local network="" address="" nonce="" message_file="" interactive="" - - # Parse the command line arguments. - # 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. - while [[ $# -gt 0 ]]; do - case "$1" in - --help) usage 0 ;; - --version) get_latest_git_commit_hash ;; - --list-networks) list_networks ;; - --network) network="$2"; shift 2 ;; - --address) address="$2"; shift 2 ;; - --nonce) nonce="$2"; shift 2 ;; - --message) message_file="$2"; shift 2 ;; - --interactive) interactive="1"; shift ;; - *) echo "Unknown option: $1" >&2; usage ;; - esac - done - - # Validate if the required parameters have the correct format. - validate_network "$network" - validate_address "$address" - - # Get the API URL and chain ID for the specified network. - local api_url=$(get_api_url "$network") - local chain_id=$(get_chain_id "$network") - local endpoint="${api_url}/api/v1/safes/${address}/multisig-transactions/?nonce=${nonce}" - - # Get the Safe multisig version. - local version=$(curl -sf "${api_url}/api/v1/safes/${address}/" | jq -r ".version // \"0.0.0\"" || echo "0.0.0") - - # If --interactive mode is enabled, the version value can be overridden by the user's input. - if [[ -n "$interactive" ]]; then - cat <<EOF + # Display the help message if no arguments are provided. + if [[ $# -eq 0 ]]; then + usage + fi + + # Initialise the CLI parameters. + local network="" + local address="" + local nonce="" + local nested_safe_address="" + local nested_safe_nonce="" + local message_file="" + local interactive="" + + # Parse the command line arguments. + # 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. + while [[ $# -gt 0 ]]; do + case "$1" in + --help) usage 0 ;; + --version) get_latest_git_commit_hash ;; + --list-networks) list_networks ;; + --network) + network="$2" + shift 2 + ;; + --address) + address="$2" + shift 2 + ;; + --nonce) + nonce="$2" + shift 2 + ;; + --nested-safe-address) + nested_safe_address="$2" + shift 2 + ;; + --nested-safe-nonce) + nested_safe_nonce="$2" + shift 2 + ;; + --message) + message_file="$2" + shift 2 + ;; + --interactive) + interactive="1" + shift + ;; + *) + echo "Unknown option: $1" >&2 + usage + ;; + esac + done + + # Validate if the required parameters have the correct format. + validate_network "$network" + validate_address "$address" + + # Get the API URL and chain ID for the specified network. + local api_url=$(get_api_url "$network") + local chain_id=$(get_chain_id "$network") + local endpoint="${api_url}/api/v1/safes/${address}/multisig-transactions/?nonce=${nonce}" + + # Get the Safe multisig version. + local version=$(curl -sf "${api_url}/api/v1/safes/${address}/" | jq -r ".version // \"0.0.0\"" || echo "0.0.0") + + # Validate the nested Safe address if provided. + local nested_safe_version="" + if [[ -n "$nested_safe_address" ]]; then + # Validate the nested Safe address. + validate_address "$nested_safe_address" + + # Get the nested Safe multisig version. + nested_safe_version=$(curl -sf "${api_url}/api/v1/safes/${nested_safe_address}/" | jq -r ".version // \"0.0.0\"" || echo "0.0.0") + fi + + # If --interactive mode is enabled, the version value can be overridden by the user's input. + if [[ -n "$interactive" ]]; then + cat <<EOF Interactive mode is enabled. You will be prompted to enter values for parameters such as \`version\`, \`to\`, \`value\`, and others. @@ -746,49 +934,69 @@ $(tput setaf 3)IMPORTANT: Please double-check your entries before proceeding.$(tput sgr0) EOF - read -rp "Enter the Safe multisig version (default: $version): " user_version - if [[ -n "$user_version" ]]; then - version="$user_version" - fi - validate_version $version - fi - - # Calculate the domain and message hashes for off-chain messages. - if [[ -n "$message_file" ]]; then - if [[ -n "$interactive" ]]; then - echo -e "${RED}Error: When calculating off-chain message hashes, do not specify the \`--interactive\` mode.${RESET}" >&2 - exit 1 - fi - if [[ -n "$nonce" ]]; then - echo -e "${RED}Error: When calculating off-chain message hashes, do not specify a nonce.${RESET}" >&2 - exit 1 - fi - calculate_offchain_message_hashes "$network" "$chain_id" "$address" "$message_file" "$version" - exit 0 - fi - - # Validate if the nonce parameter has the correct format. - # Please note that the nonce validation is intentionally placed - # after the domain and message hash calculations for off-chain - # messages, where a nonce is not required. - validate_value "$nonce" "nonce" - - # Fetch the transaction data from the API. - local response=$(curl -sf "$endpoint" || echo "{}") - - # Set the default index value for the transaction array. - local idx=0 - - if [[ -z "$interactive" ]]; then - local count=$(echo "$response" | jq -r ".count // \"0\"") - - # Inform the user that no transactions are available for the specified nonce. - if [[ $count -eq 0 ]]; then - echo "$(tput setaf 3)No transaction is available for this nonce!$(tput sgr0)" - exit 0 - # Notify the user about multiple transactions with identical nonce values and prompt for user input. - elif [[ $count -gt 1 ]]; then - cat <<EOF + read -rp "Enter the Safe multisig version (default: $version): " user_version + if [[ -n "$user_version" ]]; then + version="$user_version" + fi + validate_version $version + + # If a nested Safe is used, allow overriding its version as well. + if [[ -n "$nested_safe_address" ]]; then + read -rp "Enter the nested Safe multisig version (default: $nested_safe_version): " user_nested_version + if [[ -n "$user_nested_version" ]]; then + nested_safe_version="$user_nested_version" + fi + validate_version $nested_safe_version + fi + fi + + # Calculate the domain and message hashes for off-chain messages. + if [[ -n "$message_file" ]]; then + if [[ -n "$interactive" ]]; then + echo -e "${RED}Error: When calculating off-chain message hashes, do not specify the \`--interactive\` mode!${RESET}" >&2 + exit 1 + fi + if [[ -n "$nonce" ]]; then + echo -e "${RED}Error: When calculating off-chain message hashes, do not specify a nonce!${RESET}" >&2 + exit 1 + fi + calculate_offchain_message_hashes "$network" "$chain_id" "$address" "$message_file" "$version" + # If a nested Safe address is provided, calculate the domain and message hashes for off-chain messages + # using the nested Safe multisig address. + if [[ -n "$nested_safe_nonce" ]]; then + echo -e "${RED}Error: When calculating off-chain message hashes using a nested Safe, do not specify a nonce for the nested Safe!${RESET}" >&2 + exit 1 + elif [[ -n "$nested_safe_address" ]]; then + calculate_nested_safe_offchain_message_hashes "$chain_id" "$address" "$nested_safe_address" "$message_file" "$nested_safe_version" + fi + exit 0 + fi + + # Validate if the nonce parameter has the correct format. + # Please note that the nonce validation is intentionally placed + # after the domain and message hash calculations for off-chain + # messages, where a `nonce` (and `nested-safe-nonce`) is not required. + validate_value "$nonce" "nonce" + if [[ -n "$nested_safe_address" ]]; then + validate_value "$nested_safe_nonce" "nested-safe-nonce" + fi + + # Fetch the transaction data from the API. + local response=$(curl -sf "$endpoint" || echo "{}") + + # Set the default index value for the transaction array. + local idx=0 + + if [[ -z "$interactive" ]]; then + local count=$(echo "$response" | jq -r ".count // \"0\"") + + # Inform the user that no transactions are available for the specified nonce. + if [[ $count -eq 0 ]]; then + echo "$(tput setaf 3)No transaction is available for this nonce!$(tput sgr0)" + exit 0 + # Notify the user about multiple transactions with identical nonce values and prompt for user input. + elif [[ $count -gt 1 ]]; then + cat <<EOF $(tput setaf 3)Several transactions with identical nonce values have been detected. This occurrence is normal if you are deliberately replacing an existing transaction. However, if your Safe interface displays only a single transaction, this could indicate @@ -801,117 +1009,136 @@ $(tput setaf 2)$endpoint$(tput sgr0) Please enter the index of the array: EOF - while true; do - read -r idx - - # Validate if user input is a number. - if ! [[ $idx =~ ^[0-9]+$ ]]; then - echo "$(tput setaf 1)Error: Please enter a valid number!$(tput sgr0)" - continue - fi - - local array_value=$(echo "$response" | jq ".results[$idx]") - - if [[ $array_value == null ]]; then - echo "$(tput setaf 1)Error: No transaction found at index $idx. Please try again.$(tput sgr0)" - continue - fi - - printf "\n" - - break - done - fi - fi - - local to=$(echo "$response" | jq -r ".results[$idx].to // \"$ZERO_ADDRESS\"") - local value=$(echo "$response" | jq -r ".results[$idx].value // \"0\"") - local data=$(echo "$response" | jq -r ".results[$idx].data // \"0x\"") - local operation=$(echo "$response" | jq -r ".results[$idx].operation // \"0\"") - local safe_tx_gas=$(echo "$response" | jq -r ".results[$idx].safeTxGas // \"0\"") - local base_gas=$(echo "$response" | jq -r ".results[$idx].baseGas // \"0\"") - local gas_price=$(echo "$response" | jq -r ".results[$idx].gasPrice // \"0\"") - local gas_token=$(echo "$response" | jq -r ".results[$idx].gasToken // \"$ZERO_ADDRESS\"") - local refund_receiver=$(echo "$response" | jq -r ".results[$idx].refundReceiver // \"$ZERO_ADDRESS\"") - local data_decoded=$(echo "$response" | jq -r ".results[$idx].dataDecoded // \"0x\"") - - # If --interactive mode is enabled, the parameter values can be overridden by the user's input. - if [[ -n "$interactive" ]]; then - read -rp "Enter the \`to\` address (default: $to): " to_input - to="${to_input:-$to}" - validate_address "$to" - - read -rp "Enter the \`value\` (default: $value): " value_input - value="${value_input:-$value}" - validate_value $value "value" - - read -rp "Enter the \`data\` (default: $data): " data_input - data="${data_input:-$data}" - - while true; do - read -rp "Enter the \`operation\` (default: $operation; 0 = CALL, 1 = DELEGATECALL): " operation_input - operation_input="${operation_input:-$operation}" - if [[ "$operation_input" == "0" || "$operation_input" == "1" ]]; then - operation="$operation_input" - break - else - cat <<EOF + while true; do + read -r idx + + # Validate if user input is a number. + if ! [[ $idx =~ ^[0-9]+$ ]]; then + echo "$(tput setaf 1)Error: Please enter a valid number!$(tput sgr0)" + continue + fi + + local array_value=$(echo "$response" | jq ".results[$idx]") + + if [[ $array_value == null ]]; then + echo "$(tput setaf 1)Error: No transaction found at index $idx. Please try again.$(tput sgr0)" + continue + fi + + printf "\n" + + break + done + fi + fi + + local to=$(echo "$response" | jq -r ".results[$idx].to // \"$ZERO_ADDRESS\"") + local value=$(echo "$response" | jq -r ".results[$idx].value // \"0\"") + local data=$(echo "$response" | jq -r ".results[$idx].data // \"0x\"") + local operation=$(echo "$response" | jq -r ".results[$idx].operation // \"0\"") + local safe_tx_gas=$(echo "$response" | jq -r ".results[$idx].safeTxGas // \"0\"") + local base_gas=$(echo "$response" | jq -r ".results[$idx].baseGas // \"0\"") + local gas_price=$(echo "$response" | jq -r ".results[$idx].gasPrice // \"0\"") + local gas_token=$(echo "$response" | jq -r ".results[$idx].gasToken // \"$ZERO_ADDRESS\"") + local refund_receiver=$(echo "$response" | jq -r ".results[$idx].refundReceiver // \"$ZERO_ADDRESS\"") + local data_decoded=$(echo "$response" | jq -r ".results[$idx].dataDecoded // \"0x\"") + + # If --interactive mode is enabled, the parameter values can be overridden by the user's input. + # Overriding nested Safe transaction values is not allowed. + if [[ -n "$interactive" ]]; then + read -rp "Enter the \`to\` address (default: $to): " to_input + to="${to_input:-$to}" + validate_address "$to" + + read -rp "Enter the \`value\` (default: $value): " value_input + value="${value_input:-$value}" + validate_value $value "value" + + read -rp "Enter the \`data\` (default: $data): " data_input + data="${data_input:-$data}" + + while true; do + read -rp "Enter the \`operation\` (default: $operation; 0 = CALL, 1 = DELEGATECALL): " operation_input + operation_input="${operation_input:-$operation}" + if [[ "$operation_input" == "0" || "$operation_input" == "1" ]]; then + operation="$operation_input" + break + else + cat <<EOF $(tput setaf 3)Invalid input. Please enter either 0 (CALL) or 1 (DELEGATECALL).$(tput sgr0) EOF - fi - done - - read -rp "Enter the \`safeTxGas\` (default: $safe_tx_gas): " safe_tx_gas_input - safe_tx_gas="${safe_tx_gas_input:-$safe_tx_gas}" - validate_value $safe_tx_gas "safeTxGas" - - read -rp "Enter the \`baseGas\` (default: $base_gas): " base_gas_input - base_gas="${base_gas_input:-$base_gas}" - validate_value $base_gas "baseGas" - - read -rp "Enter the \`gasPrice\` (default: $gas_price): " gas_price_input - gas_price="${gas_price_input:-$gas_price}" - validate_value $gas_price "gasPrice" - - read -rp "Enter the \`gasToken\` (default: $gas_token): " gas_token_input - gas_token="${gas_token_input:-$gas_token}" - validate_address "$gas_token" - - read -rp "Enter the \`refundReceiver\` (default: $refund_receiver): " refund_receiver_input - refund_receiver="${refund_receiver_input:-$refund_receiver}" - validate_address "$refund_receiver" - - data_decoded="interactive" - fi - - # Warn the user if the transaction includes an untrusted delegate call. - warn_if_delegate_call "$operation" "$to" - # Check for a potential gas token attack. - check_gas_token_attack "$gas_price" "$gas_token" "$refund_receiver" - - # Calculate and display the hashes. - echo "===================================" - echo "= Selected Network Configurations =" - echo -e "===================================\n" - print_field "Network" "$network" - print_field "Chain ID" "$chain_id" true - echo "========================================" - echo "= Transaction Data and Computed Hashes =" - echo "========================================" - calculate_hashes "$chain_id" \ - "$address" \ - "$to" \ - "$value" \ - "$data" \ - "$operation" \ - "$safe_tx_gas" \ - "$base_gas" \ - "$gas_price" \ - "$gas_token" \ - "$refund_receiver" \ - "$nonce" \ - "$data_decoded" \ - "$version" + fi + done + + read -rp "Enter the \`safeTxGas\` (default: $safe_tx_gas): " safe_tx_gas_input + safe_tx_gas="${safe_tx_gas_input:-$safe_tx_gas}" + validate_value $safe_tx_gas "safeTxGas" + + read -rp "Enter the \`baseGas\` (default: $base_gas): " base_gas_input + base_gas="${base_gas_input:-$base_gas}" + validate_value $base_gas "baseGas" + + read -rp "Enter the \`gasPrice\` (default: $gas_price): " gas_price_input + gas_price="${gas_price_input:-$gas_price}" + validate_value $gas_price "gasPrice" + + read -rp "Enter the \`gasToken\` (default: $gas_token): " gas_token_input + gas_token="${gas_token_input:-$gas_token}" + validate_address "$gas_token" + + read -rp "Enter the \`refundReceiver\` (default: $refund_receiver): " refund_receiver_input + refund_receiver="${refund_receiver_input:-$refund_receiver}" + validate_address "$refund_receiver" + + data_decoded="interactive" + fi + + # Warn the user if the transaction includes an untrusted delegate call. + warn_if_delegate_call "$operation" "$to" + # Check for a potential gas token attack. + check_gas_token_attack "$gas_price" "$gas_token" "$refund_receiver" + + # Calculate and display the hashes. + echo "===================================" + echo "= Selected Network Configurations =" + echo -e "===================================\n" + print_field "Network" "$network" + print_field "Chain ID" "$chain_id" true + + echo "========================================" + echo "= Transaction Data and Computed Hashes =" + echo "========================================" + + # Add a header to indicate that this is the primary transaction when using a nested Safe. + if [[ -n "$nested_safe_address" ]]; then + echo -e "\n${BOLD}${UNDERLINE}Primary Safe Transaction Data and Computed Hashes${RESET}" + fi + + # Calculate the primary Safe transaction hash and display the data. + calculate_hashes "$chain_id" \ + "$address" \ + "$to" \ + "$value" \ + "$data" \ + "$operation" \ + "$safe_tx_gas" \ + "$base_gas" \ + "$gas_price" \ + "$gas_token" \ + "$refund_receiver" \ + "$nonce" \ + "$data_decoded" \ + "$version" \ + "true" + + # Calculate the `approveHash` transaction hash if nested Safe parameters are provided. + if [[ -n "$nested_safe_address" && -n "$nested_safe_nonce" ]]; then + calculate_nested_safe_hashes "$chain_id" "$address" "$nested_safe_address" "$nested_safe_nonce" "$global_safe_tx_hash" "$nested_safe_version" + elif [[ -n "$nested_safe_nonce" && -z "$nested_safe_address" ]]; then + echo -e "${RED}Error: The \`--nested-safe-address\` parameter is missing.${RESET}" >&2 + echo -e "${RED}Both \`--nested-safe-address\` and \`--nested-safe-nonce\` must be provided for transaction hashes!${RESET}" >&2 + exit 1 + fi } calculate_safe_hashes "$@"