Skip to content

Commit 6bdfaf5

Browse files
ipatkapcaversaccio
andauthored
✨ Add Support for Nested Safes (#26)
### 🕓 Changelog This PR adds support for 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 /** * @inheritdoc ISafe */ function approveHash(bytes32 hashToApprove) external override { if (owners[msg.sender] == address(0)) revertWithError("GS030"); approvedHashes[msg.sender][hashToApprove] = 1; emit ApproveHash(hashToApprove, msg.sender); } ``` > Reference: https://github.com/safe-global/safe-smart-account/blob/bdcfce3a76c4d1dfb256ac2ca971be7cfd6e493a/contracts/Safe.sol#L372-L379. ```solidity /** * @inheritdoc ISafe */ function execTransaction( address to, uint256 value, bytes calldata data, Enum.Operation operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address payable refundReceiver, bytes memory signatures ) external payable override returns (bool success) { onBeforeExecTransaction(to, value, data, operation, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, signatures); bytes32 txHash; // Use scope here to limit variable lifetime and prevent `stack too deep` errors { = getTransactionHash( // Transaction info to, value, data, operation, safeTxGas, // Payment info baseGas, gasPrice, gasToken, refundReceiver, // Signature info // We use the post-increment here, so the current nonce value is used and incremented afterwards. nonce++ ); checkSignatures(msg.sender, txHash, signatures); // <-- For a nested Safe `txHash` is used as input for `approveHash(bytes32)`. } ... ``` > Reference: https://github.com/safe-global/safe-smart-account/blob/bdcfce3a76c4d1dfb256ac2ca971be7cfd6e493a/contracts/Safe.sol#L108-L143. ```soldity ... } else if (v == 1) { // If v is 1 then it is an approved hash // When handling approved hashes the address of the approver is encoded into r currentOwner = address(uint160(uint256(r))); // Hashes are automatically approved by the sender of the message or when they have been pre-approved via a separate transaction if (executor != currentOwner && approvedHashes[currentOwner][dataHash] == 0) revertWithError("GS025"); ... ``` > Reference: https://github.com/safe-global/safe-smart-account/blob/bdcfce3a76c4d1dfb256ac2ca971be7cfd6e493a/contracts/Safe.sol#L318-L323. ### New Features - **New Command-Line Arguments:** - `--nested-safe-address`: Specifies the address of the nested Safe approving the transaction. - `--nested-safe-nonce`: Defines the nonce for the nested Safe's approval transaction. - **Updated Transaction Flow:** When these arguments are provided, the script: 1. Computes and displays the primary Safe transaction hashes as usual. 2. Constructs an `approveHash` transaction with: - `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`). 3. Calculates and displays the hashes for the approval transaction. - **Additional Improvements:** - Adds validation for nested Safe parameters. - Enhances the `--interactive` mode support to override the nested Safe version. - Updates the help documentation with the new parameters and an example usage for nested Safes. - Adds a CI Bash formatting step using [`shfmt`](https://github.com/mvdan/sh). - Add support of nested Safes for calculating Safe message hashes. ### Example Usage ```bash ./safe_hashes.sh --network sepolia --address 0x657ff0D4eC65D82b2bC1247b0a558bcd2f80A0f1 --nonce 4 --nested-safe-address 0x6bc56d6CE87C86CB0756c616bECFD3Cd32b09251 --nested-safe-nonce 4 ``` This will calculate and return both the primary Safe transaction hashes and the nested Safe approval transaction hashes, displaying all relevant details for each. --------- Signed-off-by: Pascal Marco Caversaccio <pascal.caversaccio@hotmail.ch> Co-authored-by: Pascal Marco Caversaccio <pascal.caversaccio@hotmail.ch>
1 parent a4d2698 commit 6bdfaf5

File tree

3 files changed

+1001
-654
lines changed

3 files changed

+1001
-654
lines changed

.github/workflows/checks.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ jobs:
1515
- ubuntu-latest
1616
node_version:
1717
- 22
18+
go_version:
19+
- 1.24
1820

1921
steps:
2022
- name: Checkout
@@ -25,9 +27,21 @@ jobs:
2527
with:
2628
node-version: ${{ matrix.node_version }}
2729

30+
- name: Set up Go
31+
uses: actions/setup-go@v5
32+
with:
33+
go-version: ${{ matrix.go_version }}
34+
cache: False
35+
36+
- name: Install `shfmt`
37+
run: go install mvdan.cc/sh/v3/cmd/shfmt@latest
38+
2839
- name: Run Prettier
2940
run: npx prettier -c '**/*.{md,yml,yaml}'
3041

42+
- name: Run `shfmt`
43+
run: shfmt -d safe_hashes.sh
44+
3145
codespell:
3246
runs-on: ${{ matrix.os }}
3347
strategy:

README.md

Lines changed: 110 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ This Bash [script](./safe_hashes.sh) calculates the Safe transaction hashes by r
2222
- [Optional: Set the New Bash as Your Default Shell](#optional-set-the-new-bash-as-your-default-shell)
2323
- [Safe Transaction Hashes](#safe-transaction-hashes)
2424
- [Interactive Mode](#interactive-mode)
25+
- [Nested Safes](#nested-safes)
2526
- [Safe Message Hashes](#safe-message-hashes)
2627
- [Trust Assumptions](#trust-assumptions)
2728
- [Community-Maintained User Interface Implementations](#community-maintained-user-interface-implementations)
@@ -68,17 +69,21 @@ This Bash [script](./safe_hashes.sh) calculates the Safe transaction hashes by r
6869
> For macOS users, please refer to the [macOS Users: Upgrading Bash](#macos-users-upgrading-bash) section.
6970
7071
```console
71-
./safe_hashes.sh [--help] [--version] [--list-networks] --network <network> --address <address> [--nonce <nonce>] [--message <file>] [--interactive]
72+
./safe_hashes.sh [--help] [--version] [--list-networks] --network <network> --address <address>
73+
[--nonce <nonce>] [--nested-safe-address <address>] [--nested-safe-nonce <nonce>]
74+
[--message <file>] [--interactive]
7275
```
7376

7477
**Options:**
7578

7679
- `--help`: Display this help message.
7780
- `--version`: Display the latest local commit hash (=version) of the script.
7881
- `--list-networks`: List all supported networks and their chain IDs.
79-
- `--network <network>`: Specify the network (e.g., `ethereum`, `polygon`).
80-
- `--address <address>`: Specify the Safe multisig address.
82+
- `--network <network>`: Specify the network (e.g., `ethereum`, `polygon`) (**required**).
83+
- `--address <address>`: Specify the Safe multisig address (**required**).
8184
- `--nonce <nonce>`: Specify the transaction nonce (required for transaction hashes).
85+
- `--nested-safe-address <address>`: Specify the nested Safe multisig address (optional for transaction hashes or off-chain message hashes).
86+
- `--nested-safe-nonce <nonce>`: Specify the nonce for the nested Safe transaction (optional for transaction hashes).
8287
- `--message <file>`: Specify the message file (required for off-chain message hashes).
8388
- `--interactive`: Use the interactive mode (optional for transaction hashes).
8489

@@ -307,6 +312,104 @@ Message hash: 0xC7E826933DA60E6AC3E2246ED0563A26A920A65BEAA9089D784AC96234141BB3
307312
Safe transaction hash: 0xc818fceb1cace51c1a4039c4c66fc73d95eccc298104c9c52debac604b9f4e04
308313
```
309314

315+
### Nested Safes
316+
317+
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:
318+
319+
```solidity
320+
function approveHash(bytes32 hashToApprove) external override {
321+
if (owners[msg.sender] == address(0)) revertWithError("GS030");
322+
approvedHashes[msg.sender][hashToApprove] = 1;
323+
emit ApproveHash(hashToApprove, msg.sender);
324+
}
325+
```
326+
327+
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:
328+
329+
```console
330+
./safe_hashes.sh --network sepolia --address 0x657ff0D4eC65D82b2bC1247b0a558bcd2f80A0f1 --nonce 4 --nested-safe-address 0x6bc56d6CE87C86CB0756c616bECFD3Cd32b09251 --nested-safe-nonce 4
331+
```
332+
333+
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:
334+
335+
```console
336+
===================================
337+
= Selected Network Configurations =
338+
===================================
339+
340+
Network: sepolia
341+
Chain ID: 11155111
342+
343+
========================================
344+
= Transaction Data and Computed Hashes =
345+
========================================
346+
347+
Primary Safe Transaction Data and Computed Hashes
348+
349+
> Transaction Data:
350+
Multisig address: 0x657ff0D4eC65D82b2bC1247b0a558bcd2f80A0f1
351+
To: 0x255C3912f91eF11bFDadd405F13144a823Da8cc5
352+
Value: 100000000000000000
353+
Data: 0x
354+
Operation: Call
355+
Safe Transaction Gas: 0
356+
Base Gas: 0
357+
Gas Price: 0
358+
Gas Token: 0x0000000000000000000000000000000000000000
359+
Refund Receiver: 0x0000000000000000000000000000000000000000
360+
Nonce: 4
361+
Encoded message: 0xbb8310d486368db6bd6f849402fdd73ad53d316b5a4b2644ad6efe0f941286d8000000000000000000000000255c3912f91ef11bfdadd405f13144a823da8cc5000000000000000000000000000000000000000000000000016345785d8a0000c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a4700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004
362+
Method: 0x (ETH Transfer)
363+
Parameters: []
364+
365+
> Hashes:
366+
Domain hash: 0x611379C19940CAEE095CDB12BEBE6A9FA9ABB74CDB1FBD7377C49A1F198DC24F
367+
Message hash: 0x565BBA8B51924FFA64953596D0A2DD5C2CAD39649F7DE0BF2C8DBC903BD03258
368+
Safe transaction hash: 0xcb8bbe7bf8f8a1f3f57658e450d07d4422356ac042d96a87ba425b19e67a78a1
369+
370+
Nested Safe `approveHash` Transaction Data and Computed Hashes
371+
372+
The specified nested Safe at 0x6bc56d6CE87C86CB0756c616bECFD3Cd32b09251 will use the following transaction to approve the primary transaction.
373+
374+
> Transaction Data:
375+
Multisig address: 0x6bc56d6CE87C86CB0756c616bECFD3Cd32b09251
376+
To: 0x657ff0D4eC65D82b2bC1247b0a558bcd2f80A0f1
377+
Value: 0
378+
Data: 0xd4d9bdcdcb8bbe7bf8f8a1f3f57658e450d07d4422356ac042d96a87ba425b19e67a78a1
379+
Operation: Call
380+
Safe Transaction Gas: 0
381+
Base Gas: 0
382+
Gas Price: 0
383+
Gas Token: 0x0000000000000000000000000000000000000000
384+
Refund Receiver: 0x0000000000000000000000000000000000000000
385+
Nonce: 4
386+
Encoded message: 0xbb8310d486368db6bd6f849402fdd73ad53d316b5a4b2644ad6efe0f941286d8000000000000000000000000657ff0d4ec65d82b2bc1247b0a558bcd2f80a0f10000000000000000000000000000000000000000000000000000000000000000873d41be4be44b68a3ad9cb19bf644be0f02392498d3a81d46d9f0741c9426640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004
387+
Method: approveHash
388+
Parameters: [
389+
{
390+
"name": "hashToApprove",
391+
"type": "bytes32",
392+
"value": "0xcb8bbe7bf8f8a1f3f57658e450d07d4422356ac042d96a87ba425b19e67a78a1"
393+
}
394+
]
395+
396+
> Hashes:
397+
Domain hash: 0x55F6C329A7834E2A4E789F5526F328FA75D14FE75B97B0001BE40CAF46CA92A1
398+
Message hash: 0xCD411EE5D49344391EF8D37B76E19DFACF505BBB20E856AC907ACB5958ECBDF0
399+
Safe transaction hash: 0x86eb3f93f2670d119a4ecb8eeaa4dafe31a28abcafe06688d47e195a3dd7abb0
400+
```
401+
402+
The nested Safe `approveHash` transaction is constructed with the following parameters:
403+
404+
- `to`: The primary Safe multisig address.
405+
- `data`: Encoded `approveHash(bytes32)` function call with the Safe transaction hash as argument.
406+
- `value`: Set to `0`.
407+
- `operation`: Set to `0` (i.e. `CALL`).
408+
- All other parameters are set to their default values (`0` or the zero address `0x0000000000000000000000000000000000000000`).
409+
410+
> [!NOTE]
411+
> The `--interactive` mode supports nested Safe transactions but only allows overriding the nested Safe version, not other transaction values in the `approveHash` transaction.
412+
310413
## Safe Message Hashes
311414

312415
> [!IMPORTANT]
@@ -365,12 +468,15 @@ Nonce:
365468
ea499f2f-fdbc-4d04-92c4-b60aba887e06
366469

367470
> Hashes:
368-
Raw message hash: 0xcb1a9208c1a7c191185938c7d304ed01db68677eea4e689d688469aa72e34236
471+
Safe message: 0xcb1a9208c1a7c191185938c7d304ed01db68677eea4e689d688469aa72e34236
369472
Domain hash: 0x611379C19940CAEE095CDB12BEBE6A9FA9ABB74CDB1FBD7377C49A1F198DC24F
370473
Message hash: 0xA5D2F507A16279357446768DB4BD47A03BCA0B6ACAC4632A4C2C96AF20D6F6E5
371474
Safe message hash: 0x1866b559f56261ada63528391b93a1fe8e2e33baf7cace94fc6b42202d16ea08
372475
```
373476

477+
> [!NOTE]
478+
> 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.
479+
374480
## Trust Assumptions
375481

376482
1. You trust my [script](./safe_hashes.sh) 😃.

0 commit comments

Comments
 (0)