Skip to content

feat: fall back to canonical MultiSendCallOnly on unserved chains - #34

Merged
aviggiano merged 1 commit into
Recon-Fuzz:mainfrom
aelmanaa:multisend-fallback
Nov 4, 2026
Merged

aviggiano merged 1 commit into
Recon-Fuzz:mainfrom
aelmanaa:multisend-fallback

Conversation

@aelmanaa

Copy link
Copy Markdown
Contributor

Relates to #33.

Problem

Safe.getMultiSendCallOnly(client, chainId) reverts with MultiSendCallOnlyNotFound for every chain the official Safe Transaction Service does not serve. So every batch path (proposeTransactions, getExecTransactionsData, simulateTransactionsNoSign, simulateTransactionsMultiSigNoSign, ...) is unusable on those chains. The canonical MultiSendCallOnly contracts (v1.4.1 and v1.3.0, deployed through the singleton factory) are often present there anyway. For example, one project targets 5 Safe testnets, and 3 of them (0G Galileo, Ink Sepolia, Plume testnet) have no transaction service.

Solution

getMultiSendCallOnly now resolves in this order:

  1. An explicit address set on the client with the new safe.withMultiSendCallOnly(address). Passing address(0) clears it.
  2. The existing table for served chains, unchanged.
  3. On the current chain only (chainId == block.chainid), the canonical v1.4.1 address if it has code, then v1.3.0.
  4. Otherwise it reverts with MultiSendCallOnlyNotFound, as before.

Notes:

  • getMultiSendCallOnly changes from pure to view because it now reads the client and checks code.
  • Client gains a new multiSendCallOnly field. Existing call sites compile unchanged.
  • The code check only runs for the current chain. Code on the forked chain says nothing about a different chainId argument.
  • The README batch section documents the fallback and the override.

Testing

  • Added 9 tests:
    • SafeMultiSendFallbackTest in test/Safe.t.sol forks Base, copies the real v1.4.1 and v1.3.0 bytecode, clears both addresses, sets vm.chainId(763373) (Ink Sepolia) and etches the code back as each case needs. The cases are: v1.4.1 only, v1.3.0 only, both (v1.4.1 wins), neither (reverts), a non-current chainId (reverts), a served chain (Base still resolves to v1.3.0), the override taking precedence, and the override cleared with address(0).
    • test/SafeSimulation.t.sol has one end-to-end batch simulation on an unserved chain id.
  • forge test (forge 1.8.7): 38 passed, 0 failed. It was 29 before.
  • Negative check: with the new resolution logic removed from src/Safe.sol, 6 of the new tests fail with MultiSendCallOnlyNotFound(763373). The other 3 are guards that pass either way. With all of src/ reverted, the tests do not compile because withMultiSendCallOnly does not exist.
  • forge fmt --check passes.

🤖 Generated with Claude Code

getMultiSendCallOnly reverted with MultiSendCallOnlyNotFound on every chain
the Safe transaction service does not serve, so batches were impossible
there even when the canonical MultiSendCallOnly is deployed.

- On the current chain, fall back to the canonical v1.4.1 address, then
  v1.3.0, if it has code. Served chains resolve as before.
- Add Client.withMultiSendCallOnly(address) to set an explicit address,
  which takes precedence; address(0) clears it.
- getMultiSendCallOnly is now view instead of pure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@aviggiano

Copy link
Copy Markdown
Collaborator

Thanks for this. Looks good to merge once CI passes. I’ll add a check for custom MultiSend addresses with no code in a follow-up, since those can report success without executing the batch. I’ll also clarify that the override must point to a trusted contract.

@aviggiano
aviggiano merged commit 3442df8 into Recon-Fuzz:main Nov 4, 2026
2 checks passed
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