Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
14 changes: 14 additions & 0 deletions foundry.lock
Original file line number Diff line number Diff line change
@@ -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"
}
}
3 changes: 3 additions & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
safe-smart-account/=lib/safe-smart-account/contracts/
solidity-http/=lib/solidity-http/src/
solidity-stringutils/=lib/solidity-stringutils/src/
2 changes: 1 addition & 1 deletion src/ISafeSmartAccount.sol
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
67 changes: 64 additions & 3 deletions src/Safe.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion test/Safe.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down