Skip to content

tools: safe-tx-verify skill — independent Safe multisig tx verifier#51

Merged
rplusq merged 4 commits into
mainfrom
tools/safe-tx-verify
Jul 14, 2026
Merged

tools: safe-tx-verify skill — independent Safe multisig tx verifier#51
rplusq merged 4 commits into
mainfrom
tools/safe-tx-verify

Conversation

@rplusq

@rplusq rplusq commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

What

Full safe-tx-verify skill at .claude/skills/safe-tx-verify/ — committed to the repo and usable as a Claude Code skill. It independently verifies a Safe (Gnosis Safe) multisig transaction before signing on a Ledger: it recomputes the EIP-712 domainHash/messageHash/safeTxHash from the raw params (never trusting the backend's hash), decodes the calldata via this repo's compiled ABIs, and labels every address against DEPLOYMENT_ADDRESSES.md.

Capabilities

  • Queue mode — verify a queued tx by nonce; cross-checks the recomputed safeTxHash against the Safe API (exit 3 + "DO NOT SIGN" on mismatch).
  • Propose mode (--to/--data/…) — verify a tx you're about to create (not yet queued); defaults to the Safe's next free nonce (on-chain nonce() reconciled against the queue).
  • Recursion into multiSend batches, Timelock execute/schedule/*Batch, and ProxyAdmin upgradeAndCall/upgradeTo — surfaces nested actions (e.g. a proxy upgrade hidden in a timelock batch).
  • Stale-nonce detection — reads on-chain nonce(), excludes/labels superseded nonces, auto-selects the single live one, lists candidates (exit 2) when several are live.
  • MultiSend singleton awareness (canonical vs eip155 variants — the gotcha that changes the hash), repo-ABI selector index with a flagged public-4byte fallback, and version-aware EIP-712 typehashes (pre/post 1.2.0 & 1.0.0).

Runtime

Dependency-free: uses cast (foundry) for keccak/decoding. Wrapped with a PEP-723 header so it runs via uv run (or plain python3):

uv run .claude/skills/safe-tx-verify/scripts/verify_safe_tx.py "oeth:0x398A2749487B2a91f2f543C01F7afD19AEE4b6b0" --nonce 48

Validation

End-to-end on OP Safe 0x398A…b6b0 nonce 48: recomputed message hash and safeTxHash match, the Safe-API cross-check passes, and forceWithdrawAll(address) decodes on StakeWeight [Optimism] with the argument address labeled from DEPLOYMENT_ADDRESSES.md.

Notes

  • .gitignore tracks .claude/skills/ while ignoring local Claude Code runtime artifacts (worktrees, locks).
  • Supersedes the earlier thin tools/ reimplementation (removed in this PR). The prior auto-review notes are addressed by the restored design: competing proposals are listed (exit 2) with stale ones flagged rather than silently picking one, and there's no baseGas no-op.

🤖 Generated with Claude Code

A uv script (PEP 723 inline deps) to verify a Safe transaction before signing on a Ledger: recomputes the EIP-712
domainHash / messageHash / safeTxHash from the raw parameters (never trusting the backend), decodes the calldata
via this repo's compiled ABIs, and labels addresses against DEPLOYMENT_ADDRESSES.md. Supports the unified Safe
Transaction Service endpoint, multi-chain prefixes, --onchain nonce/version checks, and --json.

Run: uv run tools/safe-tx-verify/verify_safe_tx.py "oeth:0x..." --nonce N

EIP-712 hashing validated against real Optimism Safe txs (recomputed safeTxHash matched the backend for every
sampled tx, incl. delegatecall multiSend batches).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jul 14, 2026

Copy link
Copy Markdown

Claude finished @rplusq's task in 3m 47s —— View job


Code Review

  • Gather context
  • Read changed files
  • Subagents: breaking changes (none), data classification (none)
  • Compile findings
Found 3 issue(s)

Issue 1: External domain URLs in hardcoded RPC list

ID: verify-safe-tx-external-rpcs-a3f1
File: tools/safe-tx-verify/verify_safe_tx.py:66
Severity: LOW
Category: security

🔒 External Domain URLs (Non-blocking)

Context:

  • Pattern: Lines 67-84 hardcode public RPC URLs for 18 chains (eth.llamarpc.com, mainnet.optimism.io, arb1.arbitrum.io, polygon-rpc.com, bsc-dataseed.binance.org, etc.) and api.safe.global on line 65
  • Risk: An MitM on a public RPC could feed crafted VERSION() or nonce responses (only relevant when --onchain is used). The hash recomputation path is unaffected.
  • Impact: Dependency on external availability; data integrity concern only for on-chain reads
  • Trigger: --onchain flag; plain hash verification is unaffected

Recommendation: Document in README that users should prefer --rpc <trusted-endpoint> for the --onchain flag; the current defaults are convenience-only.


Issue 2: Multiple proposals at same nonce — warning only goes to stderr, first tx silently used

ID: verify-safe-tx-multi-proposal-silent-4b2c
File: tools/safe-tx-verify/verify_safe_tx.py:364
Severity: MEDIUM
Category: security

Context:

  • Pattern: When len(results) > 1, a warning is printed to stderr and the script continues verifying results[0] without any prompt or --index flag
  • Risk: A signer running --json or redirecting stdout may never see the warning. The verified tx may not be the one they intend to sign — Safe UI could present a different ordering
  • Impact: In an adversarial scenario (competing proposals), the user verifies tx[0] while signing a different one
  • Trigger: Any Safe address with competing proposals at the same nonce

Recommendation: Add an --index flag, or abort unless the user explicitly selects a proposal: Fix this →

if len(results) > 1:
    print(c(f"WARNING: {len(results)} proposals at nonce {args.nonce}; pass --index N to select.", "33"), file=sys.stderr)
tx = results[args.index]  # args.index defaults to 0

Issue 3: tx.setdefault with redundant inner .get() is a no-op

ID: verify-safe-tx-setdefault-noop-9d0e
File: tools/safe-tx-verify/verify_safe_tx.py:368
Severity: LOW
Category: code_quality

Context:

  • Pattern: tx.setdefault("baseGas", tx.get("baseGas", 0)) — when "baseGas" is absent, tx.get("baseGas", 0) returns 0, reducing to tx.setdefault("baseGas", 0). When present, setdefault is a no-op regardless of the second argument.
  • Risk: None — functionally equivalent, but misleads readers into thinking it normalizes an existing value

Recommendation:

tx.setdefault("baseGas", 0)

Comment thread tools/safe-tx-verify/verify_safe_tx.py Outdated
Comment thread tools/safe-tx-verify/verify_safe_tx.py Outdated
Comment thread tools/safe-tx-verify/verify_safe_tx.py Outdated
A thin Claude Code skill (.claude/skills/safe-tx-verify) so "verify this safe tx"
runs the committed tools/safe-tx-verify script — no duplicated hashing logic, the
script stays the single trust anchor. Also gitignore .claude/ runtime artifacts
while tracking shared skills.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rplusq rplusq changed the title tools: safe-tx-verify — independent Safe multisig tx verifier tools: safe-tx-verify — independent Safe multisig tx verifier (+ skill) Jul 14, 2026
rplusq and others added 2 commits July 14, 2026 20:55
…fy baseGas default

Address auto-review: (1) when multiple proposals share a nonce, refuse to silently
verify results[0] — list every candidate's safeTxHash and exit non-zero until the
signer picks with --index (prevents verifying a different tx than is being signed,
esp. under --json/redirected stdout). (2) tx.setdefault("baseGas", 0) — the inner
.get() was a no-op.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sion, stale-nonce), drop thin reimpl

Replace the minimal tools/ reimplementation with the complete original skill at
.claude/skills/safe-tx-verify/ (committed to the repo, and usable as a Claude Code
skill). The original is far more capable: propose mode (verify a not-yet-queued tx
you're about to create, next-free-nonce aware), recursion into multiSend batches +
Timelock execute/schedule/*Batch + ProxyAdmin upgradeAndCall/upgradeTo, automatic
stale-nonce detection via on-chain nonce(), MultiSend singleton (canonical vs eip155)
labeling, repo-ABI selector index with 4byte fallback, and version-aware EIP-712
typehashes. Dependency-free (cast for keccak/decode); wrapped with a PEP-723 header so
it runs via 'uv run' (or plain python3).

Validated end-to-end: OP Safe nonce 48 -> message hash matches, safeTxHash matches the
Safe API cross-check, forceWithdrawAll decoded + address labeled from DEPLOYMENT_ADDRESSES.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rplusq rplusq changed the title tools: safe-tx-verify — independent Safe multisig tx verifier (+ skill) tools: safe-tx-verify skill — independent Safe multisig tx verifier Jul 14, 2026
@rplusq

rplusq commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator Author

@claude review

@rplusq rplusq merged commit 1d18f55 into main Jul 14, 2026
6 of 7 checks passed
@rplusq rplusq deleted the tools/safe-tx-verify branch July 14, 2026 20:18
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.

1 participant