Skip to content
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
fe633c6
WIP: V1.5.0 support
akshay-ap Nov 24, 2025
f920476
WIP: v1.5.0 fix tests
akshay-ap Nov 25, 2025
ccdd2fc
WIP: Fix tests
akshay-ap Nov 25, 2025
9df2967
WIP: v1.5.0 Fix tests
akshay-ap Nov 25, 2025
6326394
Feat: v1.5.0 event indexer test
akshay-ap Nov 26, 2025
1441925
Index event ChangedModuleGuard and add test
akshay-ap Nov 27, 2025
61b568b
v1.5.0: Update property name
akshay-ap Nov 27, 2025
e606def
v1.5.0: Update migration scripts
akshay-ap Nov 27, 2025
51b85f0
v1.5.0: Fix test
akshay-ap Nov 27, 2025
e50d9f8
v1.5.0: Fix serializer
akshay-ap Nov 27, 2025
29fab8b
v1.5.0: support both v1.4.1 and v1.5.0 in account abstraction, fix ac…
akshay-ap Nov 28, 2025
48e199f
v1.5.0: Fix tests
akshay-ap Nov 28, 2025
e1344d9
v1.5.0: Deploy v141 proxy factory in AA test
akshay-ap Nov 28, 2025
9692a7b
v1.5.0: Temp use safe-eth-py from github
akshay-ap Nov 28, 2025
f390dc2
v1.5.0: Rebase
akshay-ap Nov 28, 2025
5e3b146
Use safe-eth-py==7.17.0
akshay-ap Dec 1, 2025
b91fff1
[v1.5.0] Rename TestSafeEventsIndexerBase to SafeEventsIndexerBaseAbs…
akshay-ap Dec 4, 2025
d780f97
[v1.5.0] Add support for ProxyCreationL2 and ChainSpecificProxyCreati…
akshay-ap Dec 4, 2025
6af66c0
[v1.5.0] Add v1.5.0 proxy creation events
akshay-ap Dec 4, 2025
bde1a74
[v1.5.0] Expect proxy factory v1.4.1 to be deployed in SafeTestCaseMixin
akshay-ap Dec 4, 2025
28e1938
[v1.5.0] Fix reference to safe_tx in SafeMultisigTransactionSerializer
akshay-ap Dec 4, 2025
78b86c1
[v1.5.0] Support for v1.5.0+L2 in select_preimage_by_safe_version fun…
akshay-ap Dec 4, 2025
cd8fe50
[v1.5.0] Refactor decode_init_code to use get_safe_contract_by_versio…
akshay-ap Dec 8, 2025
6d10a69
[v1.5.0] Merge main, fix merge conflict
akshay-ap Dec 8, 2025
e705586
[v1.5.0] Temp: use github repo for safe-eth-py
akshay-ap Dec 8, 2025
6aca3f4
[v1.5.0] Use version for comparison
akshay-ap Dec 8, 2025
c13ef55
[v1.5.0] Remove property decorator from safe_contract method in SafeE…
akshay-ap Dec 8, 2025
98bdeaf
[v1.5.0] Update SafeMultisigTransactionSerializer to use Version for …
akshay-ap Dec 8, 2025
731145a
[v1.5.0] remove select_preimage_by_safe_version function, create Test…
akshay-ap Dec 8, 2025
766ddb4
Update safe_transaction_service/safe_messages/utils.py
akshay-ap Dec 8, 2025
f8cbdd5
[v1.5.0] Use renamed method select_safe_encoded_message_hash_by_safe_…
akshay-ap Dec 8, 2025
9ce46c2
[v1.5.0] Rename transaction_guard to guard across models, serializers…
akshay-ap Dec 8, 2025
53299a8
[v1.5.0] Update test_views_v2.py to use select_safe_encoded_message_h…
akshay-ap Dec 8, 2025
2eaf03b
Fix formatting
akshay-ap Dec 9, 2025
d06eb84
[v1.5.0] Remove TODO and use safe-eth-py 7.17.1
akshay-ap Dec 12, 2025
af063a3
Merge branch 'main' into feature/v1.5.0
akshay-ap Dec 12, 2025
68fe552
Merge branch 'main' into feature/v1.5.0
akshay-ap Dec 12, 2025
1a1054b
Refactor repeated code
moisses89 Dec 22, 2025
fadb2da
Fix get creation info
moisses89 Dec 22, 2025
a7b2a25
Merge branch 'main' into feature/v1.5.0
moisses89 Dec 23, 2025
ec802c5
Fix migration conflict
moisses89 Dec 23, 2025
41d6c1b
Revert "Fix get creation info"
moisses89 Dec 24, 2025
14aeb17
Disable indexing for ProxyCreationL2 and ChainSpecificProxyCreationL2
moisses89 Dec 24, 2025
f84fde4
Fix comments
moisses89 Dec 24, 2025
9ef70a4
Remove unnecessary Safe call on serializer
moisses89 Dec 30, 2025
41f3d62
Fix missing module_guard on safe last status
moisses89 Dec 30, 2025
7671150
Add missing comment
moisses89 Dec 30, 2025
5c62d9f
Add missing allow_null true when is from database
moisses89 Dec 30, 2025
b6d1083
Merge branch 'main' into feature/v1.5.0
moisses89 Jan 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,5 @@ pillow==11.3.0
psycopg[binary,pool]==3.2.13
redis[hiredis]==7.1.0
requests==2.32.5
safe-eth-py[django]==7.14.0
safe-eth-py[django]==7.17.1
web3==7.14.0
13 changes: 8 additions & 5 deletions safe_transaction_service/account_abstraction/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

from eth_typing import ChecksumAddress
from safe_eth.eth import EthereumClient
from safe_eth.eth.contracts import get_safe_V1_4_1_contract
from safe_eth.eth.contracts import get_safe_contract_by_version
from safe_eth.eth.utils import fast_to_checksum_address
from safe_eth.safe.proxy_factory import ProxyFactoryV141
from safe_eth.safe.proxy_factory import ProxyFactory


@dataclasses.dataclass(eq=True, frozen=True)
Expand Down Expand Up @@ -41,12 +41,15 @@ def decode_init_code(
- The ``ProxyFactory`` then deploys a ``Safe Proxy`` and calls ``setup`` with all the configuration parameters.
:param ethereum_client:
:return: Decoded Init Code dataclass
:raises ValueError: Problem decoding
:raises ValueError: Problem decoding or unknown factory address
"""
factory_address = fast_to_checksum_address(init_code[:20])
factory_data = init_code[20:]
proxy_factory = ProxyFactoryV141(factory_address, ethereum_client)
safe_contract = get_safe_V1_4_1_contract(ethereum_client.w3)

proxy_factory = ProxyFactory.from_address(factory_address, ethereum_client)
version = ProxyFactory.detect_version_from_address(factory_address=factory_address)
safe_contract = get_safe_contract_by_version(version=version, w3=ethereum_client.w3)

_, data = proxy_factory.contract.decode_function_input(factory_data)
initializer = data.pop("initializer")
_, safe_deployment_data = safe_contract.decode_function_input(initializer)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@
safe_4337_user_operation_hash_mock,
user_operation_mock,
user_operation_receipt_mock,
user_operation_v07_hash,
user_operation_v07_mock,
)
from safe_eth.eth.tests.mocks.mock_bundler import (
user_operation_v07_hash_1 as user_operation_v07_hash,
)
from safe_eth.eth.tests.mocks.mock_bundler import (
user_operation_v07_mock_1 as user_operation_v07_mock,
)
from safe_eth.util.util import to_0x_hex_str

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
from django.test import TestCase

from hexbytes import HexBytes
from safe_eth.eth.tests.mocks.mock_bundler import user_operation_mock
from safe_eth.eth.tests.mocks.mock_bundler import (
user_operation_mock,
)
from safe_eth.safe.tests.safe_test_case import SafeTestCaseMixin

from ..helpers import DecodedInitCode, decode_init_code


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.

with self.assertRaises(ValueError):
decode_init_code(b"", self.ethereum_client)

Expand Down
2 changes: 2 additions & 0 deletions safe_transaction_service/contracts/tx_decoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
get_safe_V1_1_1_contract,
get_safe_V1_3_0_contract,
get_safe_V1_4_1_contract,
get_safe_V1_5_0_contract,
get_uniswap_exchange_contract,
)
from safe_eth.safe.multi_send import MultiSend
Expand Down Expand Up @@ -339,6 +340,7 @@ def get_supported_abis(self) -> list[Sequence[ABIFunction]]:
get_safe_V1_1_1_contract(self.dummy_w3).abi,
get_safe_V1_3_0_contract(self.dummy_w3).abi,
get_safe_V1_4_1_contract(self.dummy_w3).abi,
get_safe_V1_5_0_contract(self.dummy_w3).abi,
]

# Order is important. If signature is the same (e.g. renaming of `baseGas`) last elements in the list
Expand Down
2 changes: 2 additions & 0 deletions safe_transaction_service/history/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -600,13 +600,15 @@ class SafeLastStatusAdmin(AdvancedAdminSearchMixin, admin.ModelAdmin):
"master_copy",
"fallback_handler",
"guard",
"module_guard",
"enabled_modules",
)
list_filter = (
"threshold",
"master_copy",
"fallback_handler",
"guard",
"module_guard",
SafeStatusModulesListFilter,
)
list_select_related = ("internal_tx__ethereum_tx", "internal_tx__decoded_tx")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
get_proxy_factory_V1_1_1_contract,
get_proxy_factory_V1_3_0_contract,
get_proxy_factory_V1_4_1_contract,
get_proxy_factory_V1_5_0_contract,
)
from safe_eth.util.util import to_0x_hex_str
from web3.contract.contract import ContractEvent
Expand Down Expand Up @@ -50,13 +51,20 @@ def contract_events(self) -> list[ContractEvent]:
proxy_factory_v_1_4_1_contract = get_proxy_factory_V1_4_1_contract(
self.ethereum_client.w3
)
proxy_factory_v_1_5_0_contract = get_proxy_factory_V1_5_0_contract(
self.ethereum_client.w3
)
return [
# event ProxyCreation(Proxy proxy)
proxy_factory_v1_1_1_contract.events.ProxyCreation(),
# event ProxyCreation(GnosisSafeProxy proxy, address singleton)
proxy_factory_v1_3_0_contract.events.ProxyCreation(),
# event ProxyCreation(SafeProxy indexed proxy, address singleton)
proxy_factory_v_1_4_1_contract.events.ProxyCreation(),
# event ProxyCreationL2(SafeProxy indexed proxy, address singleton, bytes initializer, uint256 saltNonce)
proxy_factory_v_1_5_0_contract.events.ProxyCreationL2(),
# event ChainSpecificProxyCreationL2(SafeProxy indexed proxy, address singleton, bytes initializer, uint256 saltNonce, uint256 chainId)
proxy_factory_v_1_5_0_contract.events.ChainSpecificProxyCreationL2(),
]

@property
Expand Down
73 changes: 70 additions & 3 deletions safe_transaction_service/history/indexers/safe_events_indexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@
from safe_eth.eth.contracts import (
get_proxy_factory_V1_3_0_contract,
get_proxy_factory_V1_4_1_contract,
get_proxy_factory_V1_5_0_contract,
get_safe_V1_1_1_contract,
get_safe_V1_3_0_contract,
get_safe_V1_4_1_contract,
get_safe_V1_5_0_contract,
)
from safe_eth.util.util import to_0x_hex_str
from web3.contract.contract import ContractEvent
Expand Down Expand Up @@ -54,7 +56,7 @@ def del_singleton(cls):

class SafeEventsIndexer(EventsIndexer):
"""
Indexes Gnosis Safe L2 events
Indexes Safe L2 events
"""

IGNORE_ADDRESSES_ON_LOG_FILTER = (
Expand Down Expand Up @@ -220,14 +222,29 @@ def contract_events(self) -> list[ContractEvent]:
# ProxyFactory
event ProxyCreation(GnosisSafeProxy indexed proxy, address singleton);

Safe v1.5.0 L2 Events
------------------

Note: This only includes the new events added in v1.5.0, the rest are same as from v1.4.1. No events removed in v1.5.0.

event ChangedModuleGuard(address indexed moduleGuard);

# ProxyFactory
event ProxyCreationL2(SafeProxy indexed proxy, address singleton, bytes initializer, uint256 saltNonce);
event ChainSpecificProxyCreationL2(SafeProxy indexed proxy, address singleton, bytes initializer, uint256 saltNonce, uint256 chainId);

:return: List of supported `ContractEvent`
"""
proxy_factory_v1_5_0_contract = get_proxy_factory_V1_5_0_contract(
self.ethereum_client.w3
)
proxy_factory_v1_4_1_contract = get_proxy_factory_V1_4_1_contract(
self.ethereum_client.w3
)
proxy_factory_v1_3_0_contract = get_proxy_factory_V1_3_0_contract(
self.ethereum_client.w3
)
safe_l2_v1_5_0_contract = get_safe_V1_5_0_contract(self.ethereum_client.w3)
safe_l2_v1_4_1_contract = get_safe_V1_4_1_contract(self.ethereum_client.w3)
safe_l2_v1_3_0_contract = get_safe_V1_3_0_contract(self.ethereum_client.w3)
safe_v1_1_1_contract = get_safe_V1_1_1_contract(self.ethereum_client.w3)
Expand Down Expand Up @@ -269,9 +286,13 @@ def contract_events(self) -> list[ContractEvent]:
# Changed Guard
safe_l2_v1_4_1_contract.events.ChangedGuard(),
safe_l2_v1_3_0_contract.events.ChangedGuard(),
# Change Module Guard
safe_l2_v1_5_0_contract.events.ChangedModuleGuard(),
# Change Master Copy
safe_v1_1_1_contract.events.ChangedMasterCopy(),
# Proxy creation
proxy_factory_v1_5_0_contract.events.ProxyCreationL2(),
proxy_factory_v1_5_0_contract.events.ChainSpecificProxyCreationL2(),
proxy_factory_v1_4_1_contract.events.ProxyCreation(),
proxy_factory_v1_3_0_contract.events.ProxyCreation(),
)
Expand Down Expand Up @@ -378,7 +399,12 @@ def _process_decoded_element(
safe_address=internal_tx._from, # Denormalized for efficient querying
)

if event_name == "ProxyCreation" or event_name == "SafeSetup":
if (
event_name == "ProxyCreation"
or event_name == "SafeSetup"
or event_name == "ProxyCreationL2"
or event_name == "ChainSpecificProxyCreationL2"
):
# Will ignore these events because were indexed in process_safe_creation_events
internal_tx = None
internal_tx_decoded = None
Expand Down Expand Up @@ -434,6 +460,8 @@ def _process_decoded_element(
internal_tx_decoded.function_name = "setFallbackHandler"
elif event_name == "ChangedGuard":
internal_tx_decoded.function_name = "setGuard"
elif event_name == "ChangedModuleGuard":
internal_tx_decoded.function_name = "setModuleGuard"
elif (
event_name == "SafeReceived" and not self.eth_zksync_compatible_network
): # Received ether
Expand Down Expand Up @@ -502,6 +530,16 @@ def _get_safe_creation_events(
safe_creation_events.setdefault(safe_address, []).append(
decoded_element
)
elif event_name == "ProxyCreationL2":
safe_address = decoded_element["args"].get("proxy")
safe_creation_events.setdefault(safe_address, []).append(
decoded_element
)
elif event_name == "ChainSpecificProxyCreationL2":
safe_address = decoded_element["args"].get("proxy")
safe_creation_events.setdefault(safe_address, []).append(
decoded_element
)

return safe_creation_events

Expand All @@ -510,11 +548,13 @@ def _process_safe_creation_events(
safe_addresses_with_creation_events: dict[ChecksumAddress, list[EventData]],
) -> list[InternalTx]:
"""
Process creation events (ProxyCreation and SafeSetup). They must be processed together.
Process creation events (ProxyCreation, ProxyCreationL2, ChainSpecificProxyCreationL2, and SafeSetup). They must be processed together.

Usual order is:
- SafeSetup
- ProxyCreation
- ProxyCreationL2
- ChainSpecificProxyCreationL2

:param safe_addresses_with_creation_events:
:return: Generated InternalTxs for safe creation
Expand Down Expand Up @@ -551,11 +591,17 @@ def _process_safe_creation_events(
# Find events by type (each Safe should have at most one of each)
setup_event: EventData | None = None
proxy_creation_event: EventData | None = None
proxy_creation_event_l2: EventData | None = None
chain_specific_proxy_creation_event_l2: EventData | None = None
for event in events:
if event["event"] == "SafeSetup":
setup_event = event
elif event["event"] == "ProxyCreation":
proxy_creation_event = event
elif event["event"] == "ProxyCreationL2":
proxy_creation_event_l2 = event
elif event["event"] == "ChainSpecificProxyCreationL2":
chain_specific_proxy_creation_event_l2 = event
else:
logger.error("Unexpected event type: %s", event["event"])

Expand All @@ -568,11 +614,32 @@ def _process_safe_creation_events(
call_type=None,
)
internal_txs.append(internal_tx)
if proxy_creation_event_l2:
internal_tx = self._get_internal_tx_from_decoded_element(
proxy_creation_event_l2,
contract_address=proxy_creation_event_l2["args"].get("proxy"),
tx_type=InternalTxType.CREATE.value,
call_type=None,
)
internal_txs.append(internal_tx)
if chain_specific_proxy_creation_event_l2:
internal_tx = self._get_internal_tx_from_decoded_element(
chain_specific_proxy_creation_event_l2,
contract_address=chain_specific_proxy_creation_event_l2["args"].get(
"proxy"
),
tx_type=InternalTxType.CREATE.value,
call_type=None,
)
internal_txs.append(internal_tx)

# Process SafeSetup - initializes the Safe
if setup_event:
if not proxy_creation_event:
# SafeSetup without ProxyCreation means proxy was created in a previous block
# ProxyCreationL2 or ChainSpecificProxyCreationL2 (only available v1.5.0 onwards) are not considered here because tracking ProxyCreation is enough.
# ProxyCreation is also emmited when ProxyCreationL2 or ChainSpecificProxyCreationL2 are emmited.
# See: https://github.com/safe-fndn/safe-smart-account/blob/release/v1.5.0/contracts/proxies/SafeProxyFactory.sol
logger.debug(
"[%s] Proxy was created in previous blocks, deleting the old InternalTx",
safe_address,
Expand Down
20 changes: 18 additions & 2 deletions safe_transaction_service/history/indexers/tx_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
get_safe_V1_0_0_contract,
get_safe_V1_3_0_contract,
get_safe_V1_4_1_contract,
get_safe_V1_5_0_contract,
)
from safe_eth.safe import SafeTx
from safe_eth.safe.safe_signature import SafeSignature, SafeSignatureApprovedHash
Expand Down Expand Up @@ -137,10 +138,12 @@ def __init__(
get_safe_V1_0_0_contract(dummy_w3).events.ExecutionFailed(),
get_safe_V1_3_0_contract(dummy_w3).events.ExecutionFailure(),
get_safe_V1_4_1_contract(dummy_w3).events.ExecutionFailure(),
get_safe_V1_5_0_contract(dummy_w3).events.ExecutionFailure(),
]
self.safe_tx_module_failure_events = [
get_safe_V1_3_0_contract(dummy_w3).events.ExecutionFromModuleFailure(),
get_safe_V1_4_1_contract(dummy_w3).events.ExecutionFromModuleFailure(),
get_safe_V1_5_0_contract(dummy_w3).events.ExecutionFromModuleFailure(),
]

self.safe_tx_failure_events_topics = {
Expand Down Expand Up @@ -583,10 +586,23 @@ def __process_decoded_transaction(
arguments["guard"] if arguments["guard"] != NULL_ADDRESS else None
)
if safe_last_status.guard:
logger.debug("[%s] Setting Guard", contract_address)
logger.debug("[%s] Setting TransactionGuard", contract_address)
else:
logger.debug("[%s] Unsetting Guard", contract_address)
logger.debug("[%s] Unsetting TransactionGuard", contract_address)
self.store_new_safe_status(safe_last_status, internal_tx, ["guard"])
elif function_name == "setModuleGuard":
safe_last_status.module_guard = (
arguments["moduleGuard"]
if arguments["moduleGuard"] != NULL_ADDRESS
else None
)
if safe_last_status.module_guard:
logger.debug("[%s] Setting ModuleGuard", contract_address)
else:
logger.debug("[%s] Unsetting ModuleGuard", contract_address)
self.store_new_safe_status(
safe_last_status, internal_tx, ["module_guard"]
)
elif function_name == "enableModule":
logger.debug("[%s] Enabling Module", contract_address)
safe_last_status.enabled_modules.append(arguments["module"])
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 5.2.8 on 2025-11-27 14:36

import safe_eth.eth.django.models
from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('history', '0098_internaltxdecoded_setup_idx'),
]

operations = [
migrations.AddField(
model_name='safelaststatus',
name='module_guard',
field=safe_eth.eth.django.models.EthereumAddressBinaryField(default=None, null=True),
),
migrations.AddField(
model_name='safestatus',
name='module_guard',
field=safe_eth.eth.django.models.EthereumAddressBinaryField(default=None, null=True),
)
]
Loading