Skip to content

🔨 deploy: sign safe proposals as proposer - #826

Open
cruzdanilo wants to merge 2 commits into
mainfrom
claude/safe-proposer-flow-swiiq6
Open

cruzdanilo wants to merge 2 commits into
mainfrom
claude/safe-proposer-flow-swiiq6

Conversation

@cruzdanilo

@cruzdanilo cruzdanilo commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Requested by danilo · project thread

Before: multisigPropose went through the Safe SDK (protocol-kit + api-kit). It only proposed when the deployer was a Safe owner, and otherwise printed the calldata for a manual proposal.

After: the deployer is a proposer (a delegate on the Safe Transaction Service), not an owner. The deploy script signs the SafeTx EIP-712 hash with the deployer key and posts it straight to the transaction service. The Safe SDK packages are gone.

How: multisigPropose reads the Safe's nonce and pending queue from api.safe.global/tx-service/<chain>. It skips calls that are already queued and builds a default SafeTx at the next free nonce. Then it signs with ethers.getSigner(deployer).signTypedData and posts it with sender set to the deployer. If SAFE_API_KEY is set, it's sent as a bearer token. The fallback behavior hasn't changed: unsupported chains, or a sender equal to the safe, still throw, so timelockPropose executes directly as before. Any other failure, such as the sender not being a proposer, logs the service's error and prints the manual proposal.

Checks:

  • The computed hash matches the safeTxHash of the latest real transactions on the optimism multisig (Safe 1.3.0) and the base multisig (Safe 1.4.1).
  • I ran the full flow on optimism with a throwaway key. The service accepted the request format and rejected it only on the sender check ("is not an owner or delegate"), then the manual print followed.
  • pnpm lint and pnpm test:hardhat (674 passing).

Generated by Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved multisignature transaction proposal handling by checking pending transactions and preventing duplicate proposals.
    • Added clearer error handling for failed transaction-service requests.
    • Added support for configured network-specific transaction services and safer nonce selection.
  • Chores
    • Removed unused multisignature SDK development dependencies.

@changeset-bot

changeset-bot Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: a5169e0

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@cruzdanilo cruzdanilo self-assigned this Oct 20, 2026
@coderabbitai

coderabbitai Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Walkthrough

The deployment script replaces Safe SDK operations with direct Safe Transaction Service requests and ethers typed-data signing. It checks supported chains, Safe nonce values, and pending proposals before submitting a signed transaction. The package removes the Safe SDK development dependencies.

Changes

Safe multisig proposal flow

Layer / File(s) Summary
Service mapping and HTTP requests
deploy/.utils/multisigPropose.ts, package.json
The script maps supported chains to Safe Transaction Service endpoints and uses an HTTP helper for requests. It removes three Safe SDK development dependencies.
Pending checks and proposal signing
deploy/.utils/multisigPropose.ts
The script checks the Safe nonce and pending transactions, skips matching destination and calldata, calculates a nonce, signs typed data, and submits the proposal. Sender errors retain manual fallback behavior for non-safe senders and are rethrown for safe senders.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant ProposalScript
  participant SafeTransactionService
  participant ConfiguredSender
  ProposalScript->>SafeTransactionService: Fetch Safe nonce and pending transactions
  SafeTransactionService-->>ProposalScript: Return nonce and pending transactions
  ProposalScript->>ConfiguredSender: Sign Safe transaction typed data
  ConfiguredSender-->>ProposalScript: Return signature
  ProposalScript->>SafeTransactionService: Submit signed proposal
Loading

Suggested reviewers: itofarina

Merge Risk: 🟡 Moderate · up to a5169

Safes with a pending queue spanning multiple pages can receive duplicate proposals or proposals at an occupied nonce. Fetch the full queue before merging.

Pre-merge checks: Passed 4
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly describes the main change: deploy scripts now sign Safe proposals as the proposer.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
✨ Simplify code
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.82%. Comparing base (13d9997) to head (a5169e0).

Additional details and impacted files
@@           Coverage Diff            @@
##             main     #826    +/-   ##
========================================
  Coverage   94.82%   94.82%            
========================================
  Files          32       32            
  Lines        2742     2742            
  Branches      366      469   +103     
========================================
  Hits         2600     2600            
  Misses        141      141            
  Partials        1        1            

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cruzdanilo
cruzdanilo marked this pull request as ready for review October 20, 2026 01:49
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-20T01:56:29.127536Z bfdad42 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@cruzdanilo
cruzdanilo force-pushed the claude/safe-proposer-flow-swiiq6 branch from bfdad42 to a5169e0 Compare October 20, 2026 01:51

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bfdad42712

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +50 to +52
const { results } = await api<{ results: { to: string; data: string | null; nonce: number }[] }>(
`multisig-transactions/?executed=false&nonce__gte=${nonce}`,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fetch the highest pending nonce before proposing

When this Safe has more pending transactions than the transaction service returns on its default page, results can omit the transaction with the highest nonce. The Math.max calculation then reuses an existing nonce and submits a competing proposal, rather than appending after all pending proposals as the previous getNextNonce call did. Request the highest nonce explicitly (for example, descending nonce ordering with a one-item limit) or follow pagination before calculating the new nonce.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't a regression. The previous api-kit@2.4.6 getNextNonce also read only the first page of getPendingTransactions (no limit, so the service default of 100), and its duplicate check did the same. Hitting this would take more than 100 unexecuted proposals queued on the multisig at once. For scale, the optimism multisig has 117 transactions in its entire history, and its pending queue is empty right now. Handling it properly would mean paginating both the duplicate check and the nonce scan, so I'm leaving this as is unless the maintainer wants it.


Generated by Claude Code

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 314f4bcf-eefa-4f25-bd3c-b6715d503fe9
📥 Commits

Reviewing files that changed from the base of the PR and between 13d9997 and a5169e0.

⛔ Files ignored due to path filters: 1
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing: 2
  • deploy/.utils/multisigPropose.ts
  • package.json
💤 Files with no reviewable changes: 1
  • package.json

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

gasPrice: 0,
gasToken: ZeroAddress,
refundReceiver: ZeroAddress,
nonce: Math.max(Number(nonce), ...results.map((tx) => Number(tx.nonce) + 1)),

@coderabbitai coderabbitai Bot Oct 20, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '40,85p' deploy/.utils/multisigPropose.ts
rg -n 'multisigPropose\(|already queued|pending|results.some' deploy/.utils deploy test

Repository: exactly/protocol

Length of output: 2342


Fetch the full pending queue before both checks.

results contains only the current page. A matching queued call on a later page can bypass the duplicate check and be proposed again. A higher nonce on a later page can also cause the calculation to select an occupied nonce.

Fetch every page at the shared response boundary, then use the aggregated results for both the duplicate check and nonce calculation. ordering=-nonce is not sufficient because it does not inspect later pages for duplicate calls.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same point as the Codex thread above. Both checks read only the first page (100 pending transactions), and that's also how the api-kit getPendingTransactions/getNextNonce calls this replaces behaved. For it to matter, the multisig would need more than 100 unexecuted proposals queued at once. Its whole history has 117 transactions, and the queue is empty right now. I'm leaving it as is unless the maintainer wants pagination.


Generated by Claude Code

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cruzdanilo Thanks for clarifying. Given the existing first-page behavior, I overstated this as a major issue introduced by this PR. Pagination remains an edge-case limitation, but I accept leaving it unchanged unless the maintainer requests it.


✏️ Learnings added
Learnt from: cruzdanilo
Repo: exactly/protocol PR: 826
File: deploy/.utils/multisigPropose.ts:68-68
Timestamp: 2026-10-20T01:58:06.562Z
Learning: In exactly/protocol, deploy/.utils/multisigPropose.ts retains the prior Safe api-kit first-page behavior for pending transaction checks and nonce selection. The author considers full pagination optional unless the maintainer requests it. Do not describe this inherited limitation as a regression introduced by the direct Safe Transaction Service integration.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

This branch has not been deployed

No deployments
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