From f0d87df19006132ab0964724ae2cbaa5412153a3 Mon Sep 17 00:00:00 2001 From: Pierrick Turelier Date: Fri, 31 Jul 2026 12:33:32 +0200 Subject: [PATCH] feat: support custom Safe nonce and simulate batches through the Safe MultiSigBatchBase previously always proposed at the Safe's on-chain nonce, which only advances on execution and therefore collides with proposals that are already queued. - `_proposeBatch(safe, sender)` now defaults to the `SAFE_NONCE` environment variable, falling back to the on-chain nonce, and a `_proposeBatch(safe, sender, nonce)` overload takes an explicit nonce. The batch is signed and proposed with the same nonce. - `_simulateBatch` now runs the batch through the Safe's `execTransaction` with synthetic owner approvals instead of pranking the Safe and calling each target directly, so the MultiSend encoding, the threshold check and any guard or fallback handler are exercised too. Bumps safe-utils v0.0.19 -> v0.0.22 for the nonce-aware `proposeTransactionsWithSignature` and `simulateTransactionsMultiSigNoSign`. --- lib/safe-utils | 2 +- script/MultiSigBatchBase.sol | 41 ++++++++++++++++++++++++++++++------ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/lib/safe-utils b/lib/safe-utils index 5310d6c..273945a 160000 --- a/lib/safe-utils +++ b/lib/safe-utils @@ -1 +1 @@ -Subproject commit 5310d6cd9c9d3146decfa3f8a17ac3edbeb40ca4 +Subproject commit 273945a35ade03a78648a350140aace72707d5a7 diff --git a/script/MultiSigBatchBase.sol b/script/MultiSigBatchBase.sol index d3ec99b..f6b40cf 100644 --- a/script/MultiSigBatchBase.sol +++ b/script/MultiSigBatchBase.sol @@ -2,7 +2,11 @@ pragma solidity >=0.8.20 <0.9.0; +import { Enum } from "../lib/safe-utils/lib/safe-smart-account/contracts/common/Enum.sol"; +import { OwnerManager } from "../lib/safe-utils/lib/safe-smart-account/contracts/base/OwnerManager.sol"; import { Safe } from "../lib/safe-utils/src/Safe.sol"; + +import { console } from "../lib/forge-std/src/console.sol"; import { Script } from "../lib/forge-std/src/Script.sol"; abstract contract MultiSigBatchBase is Script { @@ -17,19 +21,42 @@ abstract contract MultiSigBatchBase is Script { _data.push(data_); } - function _proposeBatch(address safe_, address sender) internal { + /// @dev The Safe's on-chain nonce only advances on execution, so it collides with already queued proposals. + /// Set the `SAFE_NONCE` environment variable to queue behind them instead of conflicting with them. + function _proposeBatch(address safe_, address sender_) internal { + _safeMultiSig.initialize(safe_); + _propose(sender_, vm.envOr("SAFE_NONCE", _safeMultiSig.getNonce())); + } + + function _proposeBatch(address safe_, address sender_, uint256 nonce_) internal { _safeMultiSig.initialize(safe_); - _safeMultiSig.proposeTransactions(_targets, _data, sender, ""); + _propose(sender_, nonce_); } + /// @dev Simulates the batch through the Safe itself, using synthetic owner approvals, so that the MultiSend + /// encoding, the threshold check and any guard or fallback handler are exercised too. function _simulateBatch(address safe_) internal { - vm.startPrank(safe_); + _safeMultiSig.initialize(safe_); + + address[] memory owners_ = OwnerManager(safe_).getOwners(); - for (uint256 i = 0; i < _targets.length; i++) { - (bool success, ) = _targets[i].call(_data[i]); - require(success, "Simulation failed"); + // NOTE: `isolate` mode runs each top-level call as its own transaction, requiring the signer to pay for gas. + for (uint256 i = 0; i < owners_.length; i++) { + vm.deal(owners_[i], owners_[i].balance + 1 ether); } - vm.stopPrank(); + require(_safeMultiSig.simulateTransactionsMultiSigNoSign(_targets, _data, owners_), "Simulation failed"); + } + + function _propose(address sender_, uint256 nonce_) private { + console.log("Safe nonce:", nonce_); + + (address to_, bytes memory data_) = _safeMultiSig.getProposeTransactionsTargetAndData(_targets, _data); + + // NOTE: Batches are executed via DelegateCall to preserve `msg.sender` across the sub-calls, and the signed + // operation must match the proposed one. + bytes memory signature_ = _safeMultiSig.sign(to_, data_, Enum.Operation.DelegateCall, sender_, nonce_, ""); + + _safeMultiSig.proposeTransactionsWithSignature(_targets, _data, sender_, signature_, nonce_); } }