Repository navigation
feat: execute multi-owner Safe transactions without the Transaction Service - #35
Merged
Merged
Conversation
…ervice getExecTransactionData builds calldata with only the sender's signature, so direct execution works only for threshold-1 Safes. Add helpers to gather approvals from several owners and build execTransaction calldata: - combineSignatures: validates 65-byte signatures (ECDSA, eth_sign v+4, approved-hash v=1) against the owners and threshold, sorts by owner - getApproveHashData: calldata for an owner's on-chain approveHash, which works with hardware wallets as an ordinary transaction - getApprovedHashSignatures: v=1 signatures for owners that approved, plus the executing owner - getExecTransactionDataWithSignatures and getExecTransactionsDataWithSignatures, with explicit-nonce overloads Tests deploy a 2-of-3 Safe through the canonical SafeProxyFactory on a Base fork. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator
|
Thanks, the signature handling and execution tests look good. I’ll merge once CI passes. I’ll follow up with separate examples for single calls and batches, plus tests showing that unapproved hashes and the wrong executor are rejected by the Safe. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Relates to #33.
Problem
Without the Safe Transaction Service, a team has to collect owner signatures itself and call
execTransaction.getExecTransactionDatabuilds calldata with one signature, the sender's, so it only works for a Safe with threshold 1. A Safe with threshold > 1 on a chain without the service (common on new L2s and testnets) has no path in the library. Hardware wallets add a second problem: a script cannot sign the safeTxHash with the device it broadcasts with (--ledger/--trezor), which is why the README describes a two-step ffi signing workaround.Solution
Two ways to gather approvals, which can be mixed:
sign(...)or any external tool.combineSignatures(safeTxHash, bytes[])checks that each 65-byte signature belongs to a current owner, rejects duplicates and counts below the threshold, and sorts by owner address ascending. It accepts ECDSA signatures (v = 27/28, including Ledger EIP-712 output fromsign), eth_sign signatures with v + 4 (v = 31/32, assignproduces for Trezor), and approved-hash entries (v = 1). Contract signatures (v = 0) are not supported.getApproveHashData(safeTxHash)builds theapproveHashcall. An owner sends it to the Safe as an ordinary transaction, so it works with--ledgeror--trezorwithout the ffi signing step.getApprovedHashSignatures(safeTxHash, executor)returns v = 1 signatures for every owner withapprovedHashes(owner, hash)set, plus the executor if it is an owner, since the Safe accepts the caller ofexecTransactionas approved.Execution calldata:
getExecTransactionDataWithSignatures(to, data, signatures[, nonce])getExecTransactionsDataWithSignatures(targets, datas, signatures[, nonce])for MultiSend batches (DelegateCall, as forproposeTransactionsWithSignature)The nonce overloads follow
proposeTransactionWithSignature(..., nonce): they let a script prepare several transactions before any of them runs.ISafeSmartAccountgainsgetOwners,getThreshold,approvedHashesandapproveHash. Existing functions are unchanged. The README has a short usage section.Testing
test/SafeDirectExec.t.soldeploys a fresh 2-of-3 Safe (v1.4.1 SafeL2) through the canonical SafeProxyFactory on a Base fork and executes WETH transfers. 10 tests:approveHashplus the executing owner executes;approveHashplus one offline signature, executed by a non-owner, executes;With
src/stashed, the new test file does not compile, because the new functions are missing. With the change:forge testpasses 39/39 (29 before), andforge fmt --checkis clean (forge 1.8.7).🤖 Generated with Claude Code