Repository navigation
feat: propose Safe transactions at the next free nonce - #59
MalteHerrmann merged 9 commits into
Conversation
The fork adds the tx-service URLs for chains that upstream does not support (1329, 1868, 2288, 3946, 4114, 4153, 5888, 25363).
The Safe on-chain nonce advances only on execution. Two proposals at one nonce compete, and only one can execute. Add `SafeNonce.next`, which reads the pending proposals from the Safe tx service and returns one above the highest pending nonce. Use it in `MultiSigBatchBase._proposeBatch(safe, sender)` and in the `SafeTimelockBatchBase` propose helpers.
- Use one name for each value in `SafeNonce.next` and `nextFrom`. - Parse the highest pending nonce only when proposals are pending. - Document the error parameters, and that `next` initializes the client. - Put the local import first in `MultiSigBatchBase`. - Track the fork's `main` branch in `.gitmodules`, as `foundry.lock` does.
|
|
Changes to gas cost
🧾 Summary (20% most significant diffs)
Full diff report 👇
|
LCOV of commit
|
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The submodule gitlink must be updated to the intended fork revision, and the external-service behavior needs coverage.
Review effort: Balanced
Findings: 1
Open (2)
What changed in this PR
Adds Safe transaction-service nonce discovery to prevent proposals from colliding with pending transactions.
Changes:
- Adds
SafeNonceand unit tests. - Uses the next available nonce for batch and timelock proposals.
- Switches
safe-utilsto the m0-platform fork.
| File | Description |
|---|---|
script/SafeNonce.sol |
Queries pending transactions and calculates the proposal nonce. |
script/MultiSigBatchBase.sol |
Uses the service-derived nonce for batches. |
script/SafeTimelockBatchBase.sol |
Signs timelock proposals with the service-derived nonce. |
test/SafeNonce.t.sol |
Tests nonce-selection logic. |
.gitmodules |
Points safe-utils at the fork. |
foundry.lock |
Records the fork revision. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…lating (#60) * fix(script): read the Safe nonce from the real chain state after simulating `_simulateBatch` executes the batch for real on the local fork. That advances the Safe nonce by one. `SafeNonce.next` then read the advanced nonce. Every script that simulates before proposing queried the service one nonce too high and proposed one slot past the free one. Wrap the simulation in a state snapshot so the proposal sees the chain as it is. Also: - Split the response parsing out of the HTTP call as `SafeNonce.fromResponse`. Fixture pages now cover the status check and the JSON paths. - Each propose helper initializes the Safe client itself. `SafeNonce.next` no longer does it as a side effect. - Add explicit-nonce overloads to the timelock propose helpers. They mirror the batch helper and bypass the transaction service. - Drop the unreachable "pending below on-chain" guard. The query already filters on `nonce__gte`. - Log the chosen nonce the same way on every propose path. * fix(script): check the state restore and restore after the timelock simulation * `MultiSigBatchBase._simulateBatch` uses `vm.revertToStateAndDelete` and requires its result. A failed restore now reverts, and the snapshot is deleted. * `TimelockBatchBase._simulateBatch` restores the chain state after the simulation. The batch's effects, e.g. a new minimum delay, do not leak into the proposal. --------- Co-authored-by: MalteHerrmann <malte.herrmann@m0.xyz>
`fromResponse` trusted the `nonce__gte` filter of the Safe transaction service. A service that ignores the filter returns a stale proposal below the on-chain nonce, e.g. the loser of a past collision, and the proposal would land at a nonce the Safe has already passed. Clamp the result to the on-chain nonce. The guard never triggers when the filter is honored.
`fromResponse` logs the pending count and the highest pending nonce, in the format the `propose-timelock-migration` skill reads. The summary line in `next` is redundant with `proposing at nonce` and goes away.


What
SafeNoncelibrary (script/SafeNonce.sol).nextreads the pending proposals from the Safe tx service and returns one above the highest pending nonce, or the on-chain nonce if none is pending. It reverts if the service does not answer with a 2xx status. The parsing is a purefromResponse, covered by fixture pages and a fuzz test.MultiSigBatchBase._proposeBatch(safe, sender)and theSafeTimelockBatchBasepropose helpers now propose at this nonce. Each has an explicit-nonce overload, for when the service cannot be queried._simulateBatchinMultiSigBatchBaseandTimelockBatchBasenow restores the chain state after the simulation. The simulation executes the batch for real on the local fork, so the Safe nonce read afterwards was one too high: with no pending proposals, the proposal landed one slot past the free nonce. A mock-Safe test fails without the restore.lib/safe-utilsfrom upstreamRecon-Fuzz/safe-utilsto them0-platform/safe-utilsfork (a2cc7c2). The fork adds tx-service URLs for more chains.Why
The Safe on-chain nonce advances only on execution. Thus, a new proposal can use the same nonce as a pending proposal, and only one of the two can execute. This moves the fix from
evm-m-suite-deploymenttocommon, so that all repos that propose Safe transactions get it (INT-437). Includes #60.