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 @@ -58,6 +58,22 @@ The second step is to take the value for the returned `bytes` and provide them w
safe.proposeTransactionWithSignature(weth, abi.encodeCall(IWETH.withdraw, (0)), sender, signature);
```

#### Batch transactions

```solidity
safe.proposeTransactions(targets, datas, sender, "m/44'/60'/0'/0/0");
```

For pre-computed signatures with hardware wallets:

```solidity
(address to, bytes memory data) = safe.getProposeTransactionsTargetAndData(targets, datas);
bytes memory signature = safe.sign(to, data, Enum.Operation.DelegateCall, sender, "m/44'/60'/0'/0/0");
safe.proposeTransactionsWithSignature(targets, datas, sender, signature);
```

**⚠️ Important**: Batch transactions require `Enum.Operation.DelegateCall` (not `Call`). Using `Call` causes signature validation errors.

### Requirements

- Foundry with FFI enabled:
Expand Down
12 changes: 10 additions & 2 deletions src/Safe.sol
Original file line number Diff line number Diff line change
Expand Up @@ -282,13 +282,21 @@ library Safe {
}

/// @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
/// @dev This can be used to propose transactions signed with a hardware wallet in a two-step process.
/// The signature must be created with Enum.Operation.DelegateCall, as batch transactions use
/// DelegateCall to preserve msg.sender across sub-calls.
///
/// WARNING: Using Enum.Operation.Call instead of DelegateCall will cause the Safe API to reject
/// your transaction with an error about an incorrect signer address. The signature will be invalid
/// because it was signed with the wrong operation type.
///
/// @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}
/// @param signature The precomputed signature for the batch of transactions. MUST be signed with
/// Enum.Operation.DelegateCall (use {sign} with DelegateCall operation).
/// Signing with Call instead of DelegateCall will result in signature validation failure.
/// @return txHash The hash of the proposed Safe transaction
function proposeTransactionsWithSignature(
Client storage self,
Expand Down
25 changes: 25 additions & 0 deletions test/Safe.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {Test, console} from "forge-std/Test.sol";
import {Safe} from "../src/Safe.sol";
import {strings} from "solidity-stringutils/strings.sol";
import {IWETH} from "./interfaces/IWETH.sol";
import {Enum} from "safe-smart-account/common/Enum.sol";

contract SafeTest is Test {
using Safe for *;
Expand Down Expand Up @@ -39,4 +40,28 @@ contract SafeTest is Test {
bytes memory data = safe.getExecTransactionData(weth, abi.encodeCall(IWETH.withdraw, (0)), foundrySigner1, "");
console.logBytes(data);
}

function test_Safe_proposeTransactionsWithSignature() public {
address weth = 0x4200000000000000000000000000000000000006;

// Create batch of transactions
address[] memory targets = new address[](2);
bytes[] memory datas = new bytes[](2);

targets[0] = weth;
datas[0] = abi.encodeCall(IWETH.withdraw, (0));

targets[1] = weth;
datas[1] = abi.encodeCall(IWETH.withdraw, (1));

// Get the target and data for signing
(address to, bytes memory data) = safe.getProposeTransactionsTargetAndData(targets, datas);

// Sign with DelegateCall operation (required for batch transactions)
vm.rememberKey(uint256(foundrySigner1PrivateKey));
bytes memory signature = safe.sign(to, data, Enum.Operation.DelegateCall, foundrySigner1, "");

// Propose transactions with the signature
safe.proposeTransactionsWithSignature(targets, datas, foundrySigner1, signature);
}
}