Skip to content

Conversation

@akshay-ap
Copy link
Member

@akshay-ap akshay-ap commented Nov 24, 2025

Safe v1.5.0 Support

This PR implements support for Safe Smart Account v1.5.0 while maintaining backward compatibility with v1.4.1. The transaction service now supports both versions simultaneously.

Changes

Database Schema

  • Renamed guard field to transaction_guard in SafeStatus and SafeLastStatus models
  • Added module_guard field to SafeStatus and SafeLastStatus models
  • Migration: 0096_guard_changes_v150.py

Event Indexing

  • Added support for ChangedModuleGuard event in Safe v1.5.0
  • Added setModuleGuard function handler in transaction processor
  • Updated event indexer tests to support v1.5.0 contract behavior

Account Abstraction

  • Updated decode_init_code helper to support both v1.4.1 and v1.5.0 proxy factories
  • Added get_contract_instances function that dynamically selects appropriate factory and contract versions based on factory address
  • Updated tests to deploy both v1.4.1 and v1.5.0 proxy factory contracts

API Changes

  • Updated SafeInfoResponseSerializer to include:
    • transaction_guard (renamed from guard)
    • module_guard (new field)
  • Updated SafeInfo.get_safe_info() to correctly map fields to SafeInfo constructor parameters

Test Updates

  • Refactored event indexer tests to handle v1.5.0 contracts
  • Updated test expectations for dual proxy factory deployment (v1.4.1 and v1.5.0)
  • Fixed SafeInfo parameter ordering bug that caused incorrect version values

Testing Gaps

  • Account abstraction decode_init_code test for v1.5.0 is currently not implemented because safe-eth-py lacks mock UserOperation data with initCode with v1.5.0 proxy factory
  • Manual testing not done yet

TODO

  • This PR depends on unreleased changes in safe-eth-py. The PR cannot be merged/reviewed until a new safe-eth-py version is published with v1.5.0 support.
  • Add ExtensibleFallabackHandler events to indexing. Should we even do it?
    Answer: As discussed with team, we are not adding it now.
  • Additional test where both v1.4.1 and v1.5.0 must be supported simultaneously
    • Added in test_views.py and test_views_v2.py
  • Manual testing
  • Replace safe-eth-py version with appropriate release version in requirements.txt

Note

Introduces full Safe v1.5.0 compatibility while preserving support for prior versions.

  • Models/API/Admin: Add module_guard to SafeStatus/SafeLastStatus (migration 0100_*), expose in SafeInfoResponseSerializer/SafeLastStatusSerializer, show/filter in admin.
  • Event indexing/processing: Track v1.5.0 ChangedModuleGuard; map to setModuleGuard; include v1.5.0 failure/module-failure events; refine creation flow (ProxyCreationL2 note).
  • Decoding: Include v1.5.0 Safe ABI in TxDecoder/SafeTxDecoder.
  • AA helpers: Decode initCode using ProxyFactory.from_address with version detection and get_safe_contract_by_version; clearer error on unknown factory.
  • Views/Tests: Add v1.5.0 deployments/mocks and split tests by Safe version; update 1271 signature path based on version; adjust expectations for dual proxy factories/master copies; add module guard assertions.
  • Deps: Bump safe-eth-py to 7.17.1.

Written by Cursor Bugbot for commit b6d1083. This will update automatically on new commits. Configure here.

@akshay-ap akshay-ap self-assigned this Nov 24, 2025
@akshay-ap akshay-ap marked this pull request as draft November 24, 2025 16:07
@akshay-ap akshay-ap changed the title WIP: V1.5.0 support V1.5.0 support Nov 28, 2025
@akshay-ap akshay-ap marked this pull request as ready for review November 28, 2025 14:03
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

This PR is being reviewed by Cursor Bugbot

Details

Your team is on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle for each member of your team.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

Uxio0
Uxio0 previously requested changes Dec 3, 2025
)
parsed_signatures = SafeSignature.parse_signature(
signature, safe_tx_hash, safe_hash_preimage=safe_tx.safe_tx_hash_preimage
signature, safe_tx_hash, safe_hash_preimage=safe_signature_hash
Copy link

Choose a reason for hiding this comment

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

Delegate signature validation missing v1.5.0 preimage handling

The validate_delegator_signature method unconditionally passes preimage as safe_hash_preimage when parsing signatures, but doesn't apply the v1.5.0 version-specific handling that was added elsewhere in this PR. For v1.5.0+ Safes using EIP-1271 signatures, the isValidSignature function expects the original message_hash rather than the Safe-encoded preimage. The transaction and message signature flows were updated to use select_safe_encoded_message_hash_by_safe_version, but the delegate signature flow was not. This causes delegate signatures from v1.5.0 Safe signers using EIP-1271 to fail validation.

Fix in Cursor Fix in Web

"""
if Version(safe_version) >= Version("1.5.0"):
return safe_message_hash
return safe_message_preimage
Copy link

Choose a reason for hiding this comment

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

Missing null check for safe version in version comparison

The select_safe_encoded_message_hash_by_safe_version function passes safe_version directly to Version() without null checking. Safe.get_version() can return None for very old Safe contracts or non-standard implementations. When None is passed to Version(), it raises a TypeError, causing API requests to fail with a 500 error. The function assumes safe_version is always a valid string but callers pass safe.get_version() which may be None.

Additional Locations (2)

Fix in Cursor Fix in Web

@moisses89
Copy link
Member

Because we are creating one internal tx by each new ProxyCreation event, the SafeServiceProvider().get_safe_creation_info(address) will break because it was expecting just one result. safe_transaction_service.history.models.InternalTx.MultipleObjectsReturned: get() returned more than one InternalTx -- it returned 2!

Done here fadb2da

@moisses89 moisses89 self-requested a review December 23, 2025 12:06
self.assertEqual(safe_contract_delegate.delegator, safe_owner.address)
self.assertEqual(safe_contract_delegate.safe_contract_id, nested_safe.address)

# TODO: Now that we have TestViewsV2V141 and TestViewsV2V150, should be create TestViewsV2V130?
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would work very similarly to 141, so I wouldn't add it at first. The TODO can be removed

)
parsed_signatures = SafeSignature.parse_signature(
signature, safe_tx_hash, safe_hash_preimage=safe_tx.safe_tx_hash_preimage
signature, safe_tx_hash, safe_hash_preimage=safe_signature_hash
Copy link

Choose a reason for hiding this comment

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

Delegate EIP-1271 signatures lack v1.5.0 version-specific hash handling

The delegate signature validation in _validate_delegate_v2_signature always passes preimage as safe_hash_preimage to SafeSignature.parse_signature, without applying version-specific handling. For v1.5.0+ Safes, isValidSignature expects the original message hash instead of the Safe-encoded preimage. Transaction signatures and safe message signatures were updated to use select_safe_encoded_message_hash_by_safe_version, but delegate signatures were not. EIP-1271 delegate signatures from v1.5.0+ Safe owners will fail validation because the wrong hash format is passed to isValidSignature.

Fix in Cursor Fix in Web

safe.get_version(), safe_message_hash, safe_message_preimage
)
safe_signatures = SafeSignature.parse_signature(
signature, safe_message_hash, safe_hash_preimage=safe_message_preimage
Copy link
Member

Choose a reason for hiding this comment

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

After reviewing the changes more carefully, I realized they are incorrect and introduce an unnecessary RPC call to fetch the Safe version.
parse_signature does not validate signatures; it only parses them and returns Signature objects (in this case, ContractSignature). Overwriting safe_message_preimage with safe_message_hash therefore just duplicates the field and adds no validation.
Signature validation actually happens in get_valid_owner_from_signatures, specifically on safe_signature of safe-eth-py here:
https://github.com/safe-global/safe-eth-py/blob/647ad2e79f347ae72693d34ac274e9047e99c3e9/safe_eth/safe/safe_signature.py#L450

)

safe_owners = get_safe_owners(safe_address)
safe_hash_preimage = select_safe_encoded_message_hash_by_safe_version(
Copy link
Member

Choose a reason for hiding this comment

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

I think that we don'r need this function select_safe_encoded_message_hash_by_safe_version

)
parsed_signatures = SafeSignature.parse_signature(
signature, safe_tx_hash, safe_hash_preimage=safe_tx.safe_tx_hash_preimage
signature, safe_tx_hash, safe_hash_preimage=safe_hash_preimage
Copy link
Member

Choose a reason for hiding this comment

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

parse_signature does not validate signatures; it only parses them and returns Signature objects (in this case, ContractSignature). Overwriting safe_message_preimage with safe_message_hash therefore just duplicates the field and adds no validation.
Signature validation actually happens in get_valid_owner_from_signatures, specifically on safe_signature of safe-eth-py here:
https://github.com/safe-global/safe-eth-py/blob/647ad2e79f347ae72693d34ac274e9047e99c3e9/safe_eth/safe/safe_signature.py#L450


class TestAccountAbstractionHelpers(SafeTestCaseMixin, TestCase):
def test_decode_init_code(self):
def test_decode_init_code_v141(self):
Copy link
Member

Choose a reason for hiding this comment

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

This changes looks unnecessary.

- Ensure that 1271 works from 1_1_1.
@moisses89 moisses89 self-requested a review December 30, 2025 11:21
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

This PR is being reviewed by Cursor Bugbot

Details

Your team is on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle for each member of your team.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

master_copy = EthereumAddressField()
fallback_handler = EthereumAddressField()
guard = EthereumAddressField(allow_null=True)
module_guard = EthereumAddressField()
Copy link

Choose a reason for hiding this comment

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

Missing allow_null=True for module_guard serializer field

The module_guard field in SafeLastStatusSerializer is defined as EthereumAddressField() without allow_null=True, but the database column (added in migration 0100) has null=True and default=None. After migration, existing SafeLastStatus records will have module_guard=None. When OwnersViewV2 or ModulesViewV2 endpoints try to serialize these records, the serializer will fail because it doesn't accept null values. Compare with the guard field on line 927 which correctly has allow_null=True.

Fix in Cursor Fix in Web

@moisses89 moisses89 dismissed Uxio0’s stale review January 5, 2026 09:31

I synced with Uxio previously to his holidays.

@moisses89 moisses89 merged commit 7e4af79 into main Jan 5, 2026
9 checks passed
@moisses89 moisses89 deleted the feature/v1.5.0 branch January 5, 2026 09:34
@github-actions github-actions bot locked and limited conversation to collaborators Jan 5, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants