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: 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 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/ 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..6264267 100644 --- a/src/Safe.sol +++ b/src/Safe.sol @@ -2,14 +2,15 @@ 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 { 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 @@ -211,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 @@ -253,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) 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 {