From 77912b200bcb40bfff8e921bb532f1598ec72725 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 2 Oct 2025 15:53:38 +0400 Subject: [PATCH 1/6] Define remappings for dependencies --- remappings.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 remappings.txt diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..3036d26 --- /dev/null +++ b/remappings.txt @@ -0,0 +1,3 @@ +safe-smart-account/=lib/safe-smart-account/contracts/ +solidity-http/=lib/solidity-http/src/ +solidity-stringutils/=lib/solidity-stringutils/src/ From a5da3508a17c354296004a6947e97296d5cb3ac2 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 2 Oct 2025 15:53:47 +0400 Subject: [PATCH 2/6] Foundry lockfile for dependencies --- foundry.lock | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 foundry.lock diff --git a/foundry.lock b/foundry.lock new file mode 100644 index 0000000..8ae97a1 --- /dev/null +++ b/foundry.lock @@ -0,0 +1,14 @@ +{ + "lib/forge-std": { + "rev": "3b20d60d14b343ee4f908cb8079495c07f5e8981" + }, + "lib/safe-smart-account": { + "rev": "bf943f80fec5ac647159d26161446ac5d716a294" + }, + "lib/solidity-http": { + "rev": "0e15051882932d4cd9f46730f1bafef8c360d1b3" + }, + "lib/solidity-stringutils": { + "rev": "4b2fcc43fa0426e19ce88b1f1ec16f5903a2e461" + } +} \ No newline at end of file From 55325570862d0b59a89c0c4d70a0e7ee49df2d90 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 2 Oct 2025 15:54:12 +0400 Subject: [PATCH 3/6] Migrate files to use remappings over relative imports --- src/ISafeSmartAccount.sol | 2 +- src/Safe.sol | 6 +++--- test/Safe.t.sol | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ISafeSmartAccount.sol b/src/ISafeSmartAccount.sol index f86dfb9..667513a 100644 --- a/src/ISafeSmartAccount.sol +++ b/src/ISafeSmartAccount.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.13; -import {Enum} from "../lib/safe-smart-account/contracts/common/Enum.sol"; +import {Enum} from "safe-smart-account/common/Enum.sol"; interface ISafeSmartAccount { function nonce() external view returns (uint256); diff --git a/src/Safe.sol b/src/Safe.sol index ffdb5aa..dfa6713 100644 --- a/src/Safe.sol +++ b/src/Safe.sol @@ -2,9 +2,9 @@ pragma solidity ^0.8.13; import {Vm} from "forge-std/Vm.sol"; -import {HTTP} from "../lib/solidity-http/src/HTTP.sol"; -import {MultiSendCallOnly} from "../lib/safe-smart-account/contracts/libraries/MultiSendCallOnly.sol"; -import {Enum} from "../lib/safe-smart-account/contracts/common/Enum.sol"; +import {HTTP} from "solidity-http/HTTP.sol"; +import {MultiSendCallOnly} from "safe-smart-account/libraries/MultiSendCallOnly.sol"; +import {Enum} from "safe-smart-account/common/Enum.sol"; import {ISafeSmartAccount} from "./ISafeSmartAccount.sol"; library Safe { diff --git a/test/Safe.t.sol b/test/Safe.t.sol index 97bfc45..d7b0b71 100644 --- a/test/Safe.t.sol +++ b/test/Safe.t.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.13; import {Test, console} from "forge-std/Test.sol"; import {Safe} from "../src/Safe.sol"; -import {strings} from "../lib/solidity-stringutils/src/strings.sol"; +import {strings} from "solidity-stringutils/strings.sol"; import {IWETH} from "./interfaces/IWETH.sol"; contract SafeTest is Test { From 786b6b0a5dc97ef01823d351e5f8e4aa807bd635 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 2 Oct 2025 15:54:19 +0400 Subject: [PATCH 4/6] Ignore forge lint warning --- src/Safe.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Safe.sol b/src/Safe.sol index dfa6713..9008c02 100644 --- a/src/Safe.sol +++ b/src/Safe.sol @@ -10,6 +10,7 @@ import {ISafeSmartAccount} from "./ISafeSmartAccount.sol"; library Safe { using HTTP for *; + /// forge-lint: disable-next-line(screaming-snake-case-const) Vm constant vm = Vm(address(bytes20(uint160(uint256(keccak256("hevm cheat code")))))); // https://github.com/safe-global/safe-smart-account/blob/release/v1.4.1/contracts/libraries/SafeStorage.sol From 1bd35bbf7c1fe53f7edd39a385bf6a7d25c3eeeb Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 2 Oct 2025 16:08:03 +0400 Subject: [PATCH 5/6] Add proposeTransaction/proposeTransactions variants with pre-computed signatures --- src/Safe.sol | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/Safe.sol b/src/Safe.sol index 9008c02..6264267 100644 --- a/src/Safe.sol +++ b/src/Safe.sol @@ -212,6 +212,35 @@ library Safe { return proposeTransaction(self, params); } + /// @notice Propose a transaction with a precomputed signature + /// @dev This can be used to propose transactions signed with a hardware wallet in a two-step process + /// + /// @param self The Safe client + /// @param to The target address for the transaction + /// @param data The data payload for the transaction + /// @param sender The address of the account that is proposing the transaction + /// @param signature The precomputed signature for the transaction, e.g. using {sign} + /// @return txHash The hash of the proposed Safe transaction + function proposeTransactionWithSignature( + Client storage self, + address to, + bytes memory data, + address sender, + bytes memory signature + ) internal returns (bytes32 txHash) { + ExecTransactionParams memory params = ExecTransactionParams({ + to: to, + value: 0, + data: data, + operation: Enum.Operation.Call, + sender: sender, + signature: signature, + nonce: getNonce(self) + }); + txHash = proposeTransaction(self, params); + return txHash; + } + function getProposeTransactionsTargetAndData(Client storage self, address[] memory targets, bytes[] memory datas) internal view @@ -254,6 +283,37 @@ library Safe { return proposeTransaction(self, params); } + /// @notice Propose multiple transactions with a precomputed signature + /// @dev This can be used to propose transactions signed with a hardware wallet in a two-step process + /// + /// @param self The Safe client + /// @param targets The list of target addresses for the transactions + /// @param datas The list of data payloads for the transactions + /// @param sender The address of the account that is proposing the transactions + /// @param signature The precomputed signature for the batch of transactions, e.g. using {sign} + /// @return txHash The hash of the proposed Safe transaction + function proposeTransactionsWithSignature( + Client storage self, + address[] memory targets, + bytes[] memory datas, + address sender, + bytes memory signature + ) internal returns (bytes32 txHash) { + (address to, bytes memory data) = getProposeTransactionsTargetAndData(self, targets, datas); + // using DelegateCall to preserve msg.sender across sub-calls + ExecTransactionParams memory params = ExecTransactionParams({ + to: to, + value: 0, + data: data, + operation: Enum.Operation.DelegateCall, + sender: sender, + signature: signature, + nonce: getNonce(self) + }); + txHash = proposeTransaction(self, params); + return txHash; + } + function getExecTransactionData(Client storage self, address to, bytes memory data, address sender) internal returns (bytes memory) From 5b0cdf3c9f5998397cc1c970eae57664faa2cd89 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 2 Oct 2025 16:16:58 +0400 Subject: [PATCH 6/6] Document usage of signatures --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 362a2a8..4b05629 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,22 @@ If you are using ledger, make sure to pass the derivation path as the last argum safe.proposeTransaction(weth, abi.encodeCall(IWETH.withdraw, (0)), sender, "m/44'/60'/0'/0/0"); ``` +Proposing a transaction/transactions using a Ledger will also require pre-computing the signature, due to a (current) limitation with forge. + +The first step is to pre-compute the signature: + +```solidity +bytes memory signature = safe.sign(weth, abi.encodeCall(IWETH.withdraw, (0)), Enum.Operation.Call, sender, "m/44'/60'/0'/0/0"); +``` + +Note that this call will fail if `forge script` is called with the `--ledger` flag, as that would block this library's contracts from utilising the same device. Instead, pass the Ledger derivation path as an argument to the script. + +The second step is to take the value for the returned `bytes` and provide them when proposing the transaction: + +```solidity +safe.proposeTransactionWithSignature(weth, abi.encodeCall(IWETH.withdraw, (0)), sender, signature); +``` + ### Requirements - Foundry with FFI enabled: