Skip to content

feat: support custom Safe nonce and simulate batches through the Safe - #53

Open
PierrickGT wants to merge 1 commit into
mainfrom
feat/safe-batch-nonce-and-simulation
Open

feat: support custom Safe nonce and simulate batches through the Safe#53
PierrickGT wants to merge 1 commit into
mainfrom
feat/safe-batch-nonce-and-simulation

Conversation

@PierrickGT

Copy link
Copy Markdown
Member

Why

MultiSigBatchBase always proposed at the Safe's on-chain nonce. That nonce only advances on execution, so any batch proposed while other transactions are already queued collides with them instead of queueing behind them.

While in here, the simulation path was an approximation: it pranked the Safe and called each target directly, which skips the MultiSend encoding, the threshold check, and any guard or fallback handler.

What

  • _proposeBatch(safe, sender) reads the nonce from the SAFE_NONCE environment variable, falling back to the Safe's on-chain nonce. _proposeBatch(safe, sender, nonce) takes one explicitly. The batch is signed and proposed with the same nonce — a mismatch there is rejected by the Safe API with a confusing "incorrect signer address" error.
  • _simulateBatch now goes through the Safe's execTransaction via simulateTransactionsMultiSigNoSign, which writes synthetic approvedHashes entries for the current owners (read from OwnerManager.getOwners()). vm.deal moved from the Safe to the owners, since under --isolate it's the pranked owner EOA that pays gas; balances are topped up rather than overwritten.
  • safe-utils v0.0.19 -> v0.0.22, which is where the nonce-aware proposeTransactionsWithSignature and simulateTransactionsMultiSigNoSign land.

The existing _proposeBatch(safe, sender) and _simulateBatch(safe) signatures are unchanged, so no downstream script needs to change.

Behavior changes worth reviewing

  1. Simulation now requires the Safe to exist in state and the chain to be in safe-utils' supported list, since it resolves MultiSendCallOnly by chain ID. The old prank loop worked on any chain against a codeless address. Every chain in ChainsBase is supported.
  2. Simulation runs at the Safe's on-chain nonce, not SAFE_NONCE — correct for a "would this execute" check, but a batch queued behind pending proposals is simulated against current state, not post-pending state.
  3. I checked the v0.0.19 -> v0.0.22 refactor (per-chain URL and MultiSend mappings became pure functions) for regressions on the paths used here: chain coverage is a superset, Plume keeps its custom transaction-service URL, and initialize / getNonce / getProposeTransactionsTargetAndData / sign are unchanged.

Testing

forge test passes (341 tests). The script base has no committed coverage, so I verified the new simulation path with a throwaway test — a SafeProxy with 3 owners and threshold 2, MultiSendCallOnly etched at the canonical address:

  • a 2-call batch executed with msg.sender at the target equal to the Safe and safe.nonce() advancing to 1, with deliberately unsorted owners sorted correctly;
  • a batch containing a reverting call surfaced as Simulation failed, with the Safe's GS013 revert data logged.

Happy to commit that test if we want script coverage in this repo — it pulls the full Safe contract into the test build, which is the only reason I left it out.

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`.
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@github-actions

Copy link
Copy Markdown

Changes to gas cost

Generated at commit: bb9ab6f5134fced233a58cfc44a96b39f2eb2c00, compared to commit: 489fae6daecd227616155e4f8cacfd17aeb3e2a1

🧾 Summary (20% most significant diffs)

Contract Method Avg (+/-) %

Full diff report 👇
Contract Deployment Cost (+/-) Method Min (+/-) % Avg (+/-) % Median (+/-) % Max (+/-) % # Calls (+/-)
ContractHelperHarness 221,136 (0) getContractFrom 697 (0) 0.00% 751 (-2) -0.27% 768 (0) 0.00% 781 (0) 0.00% 270 (0)
ERC20ExtendedHandler 766,618 (0) approve
burn
mint
transfer
transferFrom
29,595 (0)
42,191 (0)
381 (0)
477 (-55,215)
488 (0)
0.00%
0.00%
0.00%
-99.14%
0.00%
48,283 (-4)
45,694 (+8)
53,320 (+86)
65,994 (+40)
62,663 (+40)
-0.01%
+0.02%
+0.16%
+0.06%
+0.06%
51,567 (0)
44,691 (0)
62,087 (0)
60,908 (0)
61,141 (0)
0.00%
0.00%
0.00%
0.00%
0.00%
51,879 (0)
53,627 (0)
96,839 (0)
131,424 (0)
133,743 (-60)
0.00%
0.00%
0.00%
0.00%
-0.04%
12,710 (0)
12,717 (0)
12,820 (0)
13,082 (0)
12,921 (0)
ERC20ExtendedHarness 1,677,854 (0) approve
burn
mint
transfer
transferFrom
24,253 (0)
24,120 (0)
28,461 (0)
24,121 (0)
24,606 (0)
0.00%
0.00%
0.00%
0.00%
0.00%
43,250 (+1)
29,393 (+5)
48,324 (+55)
33,446 (+13)
33,213 (+13)
+0.00%
+0.02%
+0.11%
+0.04%
+0.04%
46,237 (0)
28,711 (0)
51,413 (0)
28,871 (0)
31,885 (0)
0.00%
0.00%
0.00%
0.00%
0.00%
46,537 (0)
34,695 (0)
68,873 (0)
51,955 (0)
54,671 (0)
0.00%
0.00%
0.00%
0.00%
0.00%
14,040 (+21)
13,232 (0)
16,431 (+1)
13,594 (-4)
13,689 (-1)
SignatureCheckerHarness 618,084 (0) isValidECDSASignature(address,bytes32,bytes)
isValidECDSASignature(address,bytes32,bytes32,bytes32)
isValidSignature
recoverECDSASigner(bytes32,bytes)
recoverECDSASigner(bytes32,bytes32,bytes32)
recoverECDSASigner(bytes32,uint8,bytes32,bytes32)
validateECDSASignature(address,bytes32,bytes)
validateECDSASignature(address,bytes32,bytes32,bytes32)
1,349 (0)
938 (0)
4,436 (0)
1,147 (0)
805 (0)
742 (0)
1,230 (0)
861 (0)
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
4,844 (0)
4,446 (0)
4,966 (+1)
4,549 (+1)
4,220 (+1)
4,144 (+1)
4,725 (0)
4,376 (0)
0.00%
0.00%
+0.02%
+0.02%
+0.02%
+0.02%
0.00%
0.00%
4,881 (+20)
4,470 (+4)
4,952 (0)
4,585 (+20)
4,243 (+20)
4,180 (+20)
4,762 (+20)
4,393 (+12)
+0.41%
+0.09%
0.00%
+0.44%
+0.47%
+0.48%
+0.42%
+0.27%
4,897 (0)
4,486 (0)
8,729 (0)
4,585 (0)
4,243 (0)
4,180 (0)
4,778 (0)
4,409 (0)
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
263 (0)
263 (0)
534 (0)
261 (0)
260 (0)
261 (0)
263 (0)
520 (0)
TransferHelperHarness 472,626 (-12) safeTransferExact
safeTransferFrom
32,918 (0)
34,039 (0)
0.00%
0.00%
68,103 (+1)
47,033 (+1)
+0.00%
+0.00%
68,274 (0)
34,839 (0)
0.00%
0.00%
80,919 (0)
59,931 (0)
0.00%
0.00%
514 (0)
1,028 (0)
Proxy 104,317 (-12)
AccountWithFunctionReturningNothing 124,183 (-12)
AccountWithRevertingFunction 129,187 (+12)
Bytes32StringHarness 238,480 (-12)
ERC1271MaliciousWalletMock 132,447 (+12)

@github-actions

Copy link
Copy Markdown

LCOV of commit f0d87df during Forge Coverage #163

Summary coverage rate:
  lines......: 93.8% (466 of 497 lines)
  functions..: 93.2% (150 of 161 functions)
  branches...: no data found

Files changed coverage rate: n/a

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants