Skip to content

🔨 contracts: add safe propose script#883

Open
itofarina wants to merge 1 commit intoredeployerfrom
safe-propose
Open

🔨 contracts: add safe propose script#883
itofarina wants to merge 1 commit intoredeployerfrom
safe-propose

Conversation

@itofarina
Copy link
Member

@itofarina itofarina commented Mar 12, 2026

Summary by CodeRabbit

  • New Features
    • Added a script tool to propose transactions to Safe multisig wallets.
    • Supports single or batched submissions from JSON files; validates all entries are CALLs from the same Safe.
    • Automatic chain detection for correct submission endpoint.
    • Improved error reporting for validation and API submission failures.

Open with Devin

@changeset-bot
Copy link

changeset-bot bot commented Mar 12, 2026

⚠️ No Changeset found

Latest commit: be9c2af

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

@gemini-code-assist
Copy link

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a new utility script designed to streamline the process of proposing transactions to Gnosis Safes. By reading transaction details from a JSON file, the script automates the creation and submission of single or batched transactions to the Safe Transaction Service API, enhancing efficiency and reducing manual effort for Safe operations across various EVM chains.

Highlights

  • New Safe Propose Script: Added a new Solidity script, SafePropose.s.sol, designed to facilitate the proposal of transactions to Gnosis Safes.
  • JSON Input for Transactions: The script reads transaction details from a specified JSON file, allowing for flexible and structured input of transaction data.
  • Single and Batch Transaction Support: It supports proposing both individual transactions and batched transactions, utilizing the IMultiSend interface for batch operations.
  • Safe Transaction Service API Integration: The script interacts with the Safe Transaction Service API to submit transaction proposals, handling the necessary signing and API calls.
  • Multi-Chain Compatibility: The script includes logic to determine the correct Safe Transaction Service API endpoint based on the current chain ID, supporting Ethereum, Optimism, Polygon, Base, and Arbitrum.
Changelog
  • contracts/script/SafePropose.s.sol
    • Implemented a new Solidity script for proposing transactions to Gnosis Safes.
    • Added functionality to parse transaction data from a JSON input file.
    • Integrated with the Safe Transaction Service API for submitting transaction proposals.
    • Included support for batching multiple transactions using the IMultiSend interface.
    • Provided chain-specific API endpoint resolution for supported networks.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai
Copy link

coderabbitai bot commented Mar 12, 2026

Walkthrough

Adds a new Solidity script contract SafePropose that parses JSON Safe transactions, validates a single originating Safe, and proposes either one transaction or a multisend batch to a Safe Transaction Service via signed Safe tx hashes.

Changes

Cohort / File(s) Summary
SafePropose script
contracts/script/SafePropose.s.sol
New SafePropose contract with run(string) entry point. Parses JSON into Call structs, enforces a single Safe origin, rejects non-CALL operations, handles single propose or proposeBatch (multisend encoding), computes Safe tx hash via ISafe.getTransactionHash, signs with vm.sign, builds API payload via _body, and POSTs to Safe Transaction Service. Adds ISafe and IMultiSend interfaces, Call struct, MULTISEND constant, and errors (EmptyBroadcast, NotACall, SenderMismatch, ProposalFailed, UnsupportedChain). Review multisend packing, signature creation, and chain-prefix logic.
Spelling dictionary
cspell.json
Adds two words "MULTISEND" and "oeth" to the allowed words list (vocabulary update).

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant File as JSON File
    participant Script as SafePropose
    participant Safe as ISafe
    participant API as Safe Tx Service
    participant Multi as IMultiSend

    User->>Script: run(filePath)
    Script->>File: read & parse JSON -> Call[]
    Script->>Script: validate single sender

    alt single transaction
        Script->>Safe: nonce()
        Safe-->>Script: nonce
        Script->>Safe: getTransactionHash(to,value,data,op,...,nonce)
        Safe-->>Script: safeTxHash
        Script->>Script: vm.sign(safeTxHash) -> signature
        Script->>API: POST proposal (body with safeTxHash, signature, nonce, sender)
        API-->>Script: 201 / error
    else multisend batch
        Script->>Script: encode multisend payload (calls -> bytes)
        Script->>Multi: multiSend payload (encoded)
        Script->>Safe: nonce()
        Safe-->>Script: nonce
        Script->>Safe: getTransactionHash(MULTISEND,0,multisendPayload,op,...,nonce)
        Safe-->>Script: safeTxHash
        Script->>Script: vm.sign(safeTxHash) -> signature
        Script->>API: POST proposal (body with safeTxHash, signature, nonce, sender)
        API-->>Script: 201 / error
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title accurately summarizes the main change: adding a new SafePropose script contract to the contracts directory.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch safe-propose
📝 Coding Plan
  • Generate coding plan for human review comments

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 and usage tips.

gemini-code-assist[bot]

This comment was marked as resolved.

@sentry
Copy link

sentry bot commented Mar 12, 2026

✅ All tests passed.

@itofarina itofarina self-assigned this Mar 12, 2026
coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@itofarina itofarina marked this pull request as ready for review March 13, 2026 19:57
@itofarina itofarina requested a review from cruzdanilo as a code owner March 13, 2026 19:57
Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 4 potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

using stdJson for string;
using Surl for string;

IMultiSend internal constant MULTISEND = IMultiSend(0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526); // github.com/safe-global/safe-deployments v1.4.1

Choose a reason for hiding this comment

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

🟡 Explanatory comment violates the no-comments rule from AGENTS.md

The comment // github.com/safe-global/safe-deployments v1.4.1 on the MULTISEND constant is explanatory prose, not a static analysis annotation or a TODO/HACK/FIXME marker. AGENTS.md explicitly states: "this codebase does not use comments. the only exception is static analysis annotations (@ts-expect-error, eslint-disable, slither-disable, solhint-disable, cspell:ignore) and TODO/HACK/FIXME markers. everything else—jsdoc, explanatory prose, region markers, inline labels—is noise that masks unclear code." All other // comments across contracts/src/ and contracts/script/ are strictly static analysis annotations (slither-disable, solhint-disable, forge-lint), confirming this is a violation. The comment could be replaced with a cspell:ignore annotation if needed for the spell checker.

Suggested change
IMultiSend internal constant MULTISEND = IMultiSend(0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526); // github.com/safe-global/safe-deployments v1.4.1
IMultiSend internal constant MULTISEND = IMultiSend(0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526);
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

"mload",
"modelcontextprotocol",
"moti",
"MULTISEND",

Choose a reason for hiding this comment

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

🟡 One-off constant name "MULTISEND" added to global cspell dictionary instead of inline cspell:ignore

"MULTISEND" only appears in the single file contracts/script/SafePropose.s.sol as a constant identifier. AGENTS.md states: "only add words to cspell.json when the term is a real project-relevant word that appears broadly (e.g., a protocol name, a library, a domain term)" and "one-off occurrences (variable names, company names, urls, hashes, identifiers) stay as inline cspell:ignore — never pollute the global dictionary with them." Since Solidity supports // comments, an inline // cspell:ignore MULTISEND on contracts/script/SafePropose.s.sol:16 is the correct approach.

Prompt for agents
Remove "MULTISEND" from cspell.json line 109. Instead, add an inline cspell:ignore annotation on the line where the constant is declared in contracts/script/SafePropose.s.sol line 16. The line should become:

  IMultiSend internal constant MULTISEND = IMultiSend(0x38869bf66a61cF6bDB996A6aE40D5853Fd43B526); // cspell:ignore MULTISEND

Note: this also addresses the separate issue of the explanatory comment on that same line, which should be removed per the no-comments rule.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

"nfmelendez",
"nomiclabs",
"nystrom",
"oeth",

Choose a reason for hiding this comment

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

🟡 One-off chain prefix "oeth" added to global cspell dictionary instead of inline cspell:ignore

"oeth" is a Safe Transaction Service chain prefix string that only appears in contracts/script/SafePropose.s.sol:115. AGENTS.md states: "one-off occurrences (variable names, company names, urls, hashes, identifiers) stay as inline cspell:ignore — never pollute the global dictionary with them." This is a one-off identifier (Optimism's chain prefix abbreviation), not a broadly-used project domain term. An inline // cspell:ignore oeth annotation on contracts/script/SafePropose.s.sol:115 is the correct approach.

Prompt for agents
Remove "oeth" from cspell.json line 115. Instead, add an inline cspell:ignore annotation on the line where the string appears in contracts/script/SafePropose.s.sol line 115. The line should become:

    if (block.chainid == 10) return "oeth"; // cspell:ignore oeth
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +113 to +120
function _chainPrefix() internal view returns (string memory) {
if (block.chainid == 1) return "eth";
if (block.chainid == 10) return "oeth";
if (block.chainid == 137) return "pol";
if (block.chainid == 8453) return "base";
if (block.chainid == 42_161) return "arb1";
revert UnsupportedChain();
}

Choose a reason for hiding this comment

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

🚩 Safe API v2 URL format and chain prefixes should be verified

The URL pattern https://api.safe.global/tx-service/{prefix}/api/v2/safes/{safe}/multisig-transactions/ and the chain prefix mapping (eth=1, oeth=10, pol=137, base=8453, arb1=42161) at contracts/script/SafePropose.s.sol:113-120 correspond to the Safe Transaction Service's newer API format. These prefixes are not easily verifiable from the codebase alone. If Safe changes their API URL structure or chain prefixes, this would silently fail with a non-201 status (caught by the ProposalFailed revert). Worth verifying against current Safe API documentation.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link

@coderabbitai coderabbitai bot left a comment

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: Pro

Run ID: 66bc0413-5e67-4e27-bd51-08fba140d16c

📥 Commits

Reviewing files that changed from the base of the PR and between 923d1e3 and be9c2af.

📒 Files selected for processing (2)
  • contracts/script/SafePropose.s.sol
  • cspell.json

Comment on lines +62 to +66
{
(uint8 v, bytes32 r, bytes32 s) = vm.sign(safeTxHash);
sender = ecrecover(safeTxHash, v, r, s);
signature = abi.encodePacked(r, s, v);
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider validating ecrecover result.

ecrecover returns address(0) if the signature or hash is malformed. While unlikely in normal operation, adding a guard would provide a clearer error message than a downstream API failure.

🛡️ Proposed fix
     {
       (uint8 v, bytes32 r, bytes32 s) = vm.sign(safeTxHash);
       sender = ecrecover(safeTxHash, v, r, s);
+      require(sender != address(0), "Invalid signature");
       signature = abi.encodePacked(r, s, v);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{
(uint8 v, bytes32 r, bytes32 s) = vm.sign(safeTxHash);
sender = ecrecover(safeTxHash, v, r, s);
signature = abi.encodePacked(r, s, v);
}
{
(uint8 v, bytes32 r, bytes32 s) = vm.sign(safeTxHash);
sender = ecrecover(safeTxHash, v, r, s);
require(sender != address(0), "Invalid signature");
signature = abi.encodePacked(r, s, v);
}

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