Skip to content

Commit

Permalink
4337: Ignore UserOperations for v0.7.0 entrypoint (#2024)
Browse files Browse the repository at this point in the history
- It changes a lot of logic and it's not fully supported in other networks but Sepolia
- We should wait to implement #2011
  • Loading branch information
Uxio0 authored May 8, 2024
1 parent c3569a8 commit bb00fcf
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 21 deletions.
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,5 @@ psycogreen==1.0.2
psycopg2==2.9.9
redis==5.0.4
requests==2.31.0
safe-eth-py[django]==6.0.0b27
safe-eth-py[django]==6.0.0b28
web3==6.17.0
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
BundlerClientException,
UserOperation,
UserOperationReceipt,
UserOperationV07,
)
from gnosis.eth.utils import fast_to_checksum_address
from gnosis.safe.account_abstraction import SafeOperation
Expand All @@ -36,6 +37,10 @@ class AaProcessorServiceException(Exception):
pass


class UserOperationNotSupportedException(Exception):
pass


class ExecutionFromSafeModuleNotDetected(AaProcessorServiceException):
pass

Expand Down Expand Up @@ -67,16 +72,16 @@ def __init__(
self.bundler_client = bundler_client
self.supported_entry_points = supported_entry_points

def get_user_operation_logs(
def get_user_operation_hashes_from_logs(
self, safe_address: ChecksumAddress, logs: [Sequence[LogReceipt]]
) -> List[LogReceipt]:
) -> List[HexBytes]:
"""
:param safe_address:
:param logs:
:return: Logs for ``UserOperation`` from entrypoint if detected
:return: ``UserOperations`` hashes if detected
"""
return [
log
HexBytes(log["topics"][1])
for log in logs
if (
len(log["topics"]) == USER_OPERATION_NUMBER_TOPICS
Expand Down Expand Up @@ -251,49 +256,53 @@ def index_user_operation_receipt(
def index_user_operation(
self,
safe_address: ChecksumAddress,
log: LogReceipt,
user_operation_hash: HexBytes,
ethereum_tx: history_models.EthereumTx,
) -> Tuple[UserOperationModel, UserOperation]:
"""
Index ``UserOperation``, ``SafeOperation`` and ``UserOperationReceipt`` for the given ``UserOperation`` log
:param safe_address: to prevent indexing UserOperations from other address
:param log: log event with the ``UserOperation`` emitted by the entryPoint
:param user_operation_hash: hash for the ``UserOperation``
:param ethereum_tx: Stored EthereumTx in database containing the ``UserOperation``
:return: tuple of ``UserOperationModel`` and ``UserOperation``
"""
user_operation_hash = HexBytes(log["topics"][1]).hex()
user_operation_hash_hex = user_operation_hash.hex()
# If the UserOperationReceipt is present, UserOperation was already processed and mined
if self.is_user_operation_indexed(user_operation_hash):
if self.is_user_operation_indexed(user_operation_hash_hex):
logger.warning(
"[%s] user-operation-hash=%s receipt was already indexed",
safe_address,
user_operation_hash,
user_operation_hash_hex,
)
else:
logger.debug(
"[%s] Retrieving UserOperation from Bundler with user-operation-hash=%s on tx-hash=%s",
safe_address,
user_operation_hash,
user_operation_hash_hex,
ethereum_tx.tx_hash,
)
user_operation = self.bundler_client.get_user_operation_by_hash(
user_operation_hash
user_operation_hash_hex
)
if not user_operation:
self.bundler_client.get_user_operation_by_hash.cache_clear()
raise BundlerClientException(
f"user-operation={user_operation_hash} returned `null`"
f"user-operation={user_operation_hash_hex} returned `null`"
)
if isinstance(user_operation, UserOperationV07):
raise UserOperationNotSupportedException(
f"user-operation={user_operation_hash_hex} for EntryPoint v0.7.0 is not supported"
)

try:
user_operation_model = UserOperationModel.objects.get(
hash=user_operation_hash
hash=user_operation_hash_hex
)
logger.debug(
"[%s] Updating UserOperation with user-operation=%s on tx-hash=%s",
safe_address,
user_operation_hash,
user_operation_hash_hex,
ethereum_tx.tx_hash,
)
user_operation_model.signature = user_operation.signature
Expand All @@ -303,12 +312,12 @@ def index_user_operation(
logger.debug(
"[%s] Storing UserOperation with user-operation=%s on tx-hash=%s",
safe_address,
user_operation_hash,
user_operation_hash_hex,
ethereum_tx.tx_hash,
)
user_operation_model = UserOperationModel.objects.create(
ethereum_tx=ethereum_tx,
hash=user_operation_hash,
hash=user_operation_hash_hex,
sender=user_operation.sender,
nonce=user_operation.nonce,
init_code=user_operation.init_code,
Expand Down Expand Up @@ -344,17 +353,27 @@ def process_aa_transaction(
:param ethereum_tx: EthereumTx to check for UserOperations
:return: Number of detected ``UserOperations`` in transaction
"""
aa_logs = self.get_user_operation_logs(safe_address, ethereum_tx.logs)
number_detected_user_operations = len(aa_logs)
user_operation_hashes = self.get_user_operation_hashes_from_logs(
safe_address, ethereum_tx.logs
)
number_detected_user_operations = len(user_operation_hashes)
if not self.bundler_client:
logger.debug(
"Detected 4337 User Operation but bundler client was not configured"
)
return number_detected_user_operations

for log in aa_logs:
for user_operation_hash in user_operation_hashes:
try:
self.index_user_operation(safe_address, log, ethereum_tx)
self.index_user_operation(
safe_address, user_operation_hash, ethereum_tx
)
except UserOperationNotSupportedException as exc:
logger.error(
"[%s] Error processing user-operation: %s",
safe_address,
exc,
)
except BundlerClientException as exc:
logger.error(
"[%s] Error retrieving user-operation from bundler API: %s",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

from django.test import TestCase

from eth_account import Account

from gnosis.eth import EthereumClient
from gnosis.eth.account_abstraction import BundlerClient
from gnosis.eth.account_abstraction import UserOperation as UserOperationClass
Expand All @@ -13,6 +15,8 @@
safe_4337_user_operation_hash_mock,
user_operation_mock,
user_operation_receipt_mock,
user_operation_v07_hash,
user_operation_v07_mock,
)

from safe_transaction_service.account_abstraction.services import (
Expand All @@ -25,6 +29,8 @@
from ...models import SafeOperationConfirmation as SafeOperationConfirmationModel
from ...models import UserOperation as UserOperationModel
from ...models import UserOperationReceipt as UserOperationReceiptModel
from ...services.aa_processor_service import UserOperationNotSupportedException
from ...utils import get_bundler_client
from ..mocks import (
aa_chain_id,
aa_expected_safe_operation_hash,
Expand All @@ -38,13 +44,16 @@ class TestAaProcessorService(TestCase):

def setUp(self):
super().setUp()
get_bundler_client.cache_clear()
get_aa_processor_service.cache_clear()
with self.settings(ETHEREUM_4337_BUNDLER_URL="https://localhost"):
# Bundler must be defined so it's initialized and it can be mocked
self.aa_processor_service = get_aa_processor_service()
self.assertIsNotNone(self.aa_processor_service.bundler_client)

def tearDown(self):
super().tearDown()
get_bundler_client.cache_clear()
get_aa_processor_service.cache_clear()

@mock.patch.object(
Expand Down Expand Up @@ -96,3 +105,52 @@ def test_process_aa_transaction(
user_operation_confirmation_model.owner,
"0x5aC255889882aCd3da2aA939679E3f3d4cea221e",
)

@mock.patch.object(
BundlerClient,
"get_user_operation_receipt",
autospec=True,
return_value=UserOperationReceiptClass.from_bundler_response(
user_operation_receipt_mock["result"]
),
)
@mock.patch.object(
BundlerClient,
"get_user_operation_by_hash",
autospec=True,
return_value=UserOperationClass.from_bundler_response(
user_operation_v07_hash.hex(), user_operation_v07_mock["result"]
),
)
@mock.patch.object(
EthereumClient,
"get_chain_id",
autospec=True,
return_value=aa_chain_id, # Needed for hashes to match
)
def test_process_aa_transaction_entrypoint_V07(
self,
get_chain_id_mock: MagicMock,
get_user_operation_by_hash_mock: MagicMock,
get_user_operation_receipt_mock: MagicMock,
):
"""
Entrypoint v0.7.0 endpoints should be ignored
"""
ethereum_tx = history_factories.EthereumTxFactory(
logs=[clean_receipt_log(log) for log in aa_tx_receipt_mock["logs"]]
)
with self.assertRaisesMessage(
UserOperationNotSupportedException, "for EntryPoint v0.7.0 is not supported"
):
self.aa_processor_service.index_user_operation(
Account.create().address, # Not relevant
user_operation_v07_hash,
ethereum_tx,
)

self.aa_processor_service.process_aa_transaction(aa_safe_address, ethereum_tx)
self.assertEqual(UserOperationModel.objects.count(), 0)
self.assertEqual(SafeOperationModel.objects.count(), 0)
self.assertEqual(UserOperationReceiptModel.objects.count(), 0)
self.assertEqual(SafeOperationConfirmationModel.objects.count(), 0)

0 comments on commit bb00fcf

Please sign in to comment.