Repository: rotki/rotki

Issue:
Handle the 0 token transfer phishing scams
## Problem Definition

There is this phishing scam where someone can make it appear as if you transferred tokens (USDC and more) out of your wallet to a specific address.

More info: https://cointelegraph.com/news/scammers-are-targeting-crypto-users-with-new-zero-value-transferfrom-trick

Unfortunately our UI is also a victim to that. Case in point for our very own rotki.eth

![2023-03-09_12-58](https://user-images.githubusercontent.com/1658405/224016931-c651e8cb-a2e3-48f3-8a96-baf7d5c554d0.png)

The transaction here: https://etherscan.io/tx/0xb45ef1a202a8d9e983cf59129d28f79057969bb822f62e4b7d9f1ac8853d23ed

How this works is quite silly and it's a flaw of many token contracts, including USDC. The `transferFrom` function can be called by anyone as long as the approval of the address is enough for the amount. So the attacker's address has approval for 0 amount (the default), so they can initiate a transfer of 0 amount of tokens. And this transfer is valid and logged on-chain.

## Task

I think for us the fix is easy. Zero value token transfers are useless. Even if you actually made them yourself. I would opt for just not generating an event in that case. And somehow also ignoring the transaction (as the transaction would be a transaction with no events).


Discussion:
On L2s, due to much cheaper fees, people will probably be hit by this a lot. For example this is how my ERC20 transactions on BSC look like. On this page there are 9 real transactions and 16 phishing... 

![image](https://user-images.githubusercontent.com/63492346/224394015-f2d2c143-e2b9-44de-9229-97879188b3d5.png)


Resolving patch:
diff --git a/rotkehlchen/chain/evm/decoding/base.py b/rotkehlchen/chain/evm/decoding/base.py
--- a/rotkehlchen/chain/evm/decoding/base.py
+++ b/rotkehlchen/chain/evm/decoding/base.py
@@ -6,6 +6,7 @@
 from rotkehlchen.accounting.structures.types import HistoryEventSubType, HistoryEventType
 from rotkehlchen.chain.evm.decoding.constants import OUTGOING_EVENT_TYPES
 from rotkehlchen.constants import ONE
+from rotkehlchen.constants.misc import ZERO
 from rotkehlchen.types import ChecksumEvmAddress, Timestamp
 
 if TYPE_CHECKING:
@@ -173,6 +174,9 @@ def decode_erc20_721_transfer(
         else:
             return None  # unknown kind
 
+        if amount == ZERO:
+            return None  # Zero transfers are useless, so ignoring them
+
         return self.make_event(
             tx_hash=transaction.tx_hash,
             sequence_index=self.get_sequence_index(tx_log),
diff --git a/rotkehlchen/chain/evm/decoding/decoder.py b/rotkehlchen/chain/evm/decoding/decoder.py
--- a/rotkehlchen/chain/evm/decoding/decoder.py
+++ b/rotkehlchen/chain/evm/decoding/decoder.py
@@ -10,7 +10,11 @@
 
 from rotkehlchen.accounting.structures.balance import Balance
 from rotkehlchen.accounting.structures.base import HistoryBaseEntry
-from rotkehlchen.accounting.structures.types import HistoryEventSubType, HistoryEventType
+from rotkehlchen.accounting.structures.types import (
+    ActionType,
+    HistoryEventSubType,
+    HistoryEventType,
+)
 from rotkehlchen.assets.asset import AssetWithOracles, EvmToken
 from rotkehlchen.assets.utils import TokenSeenAt, get_or_create_evm_token
 from rotkehlchen.chain.ethereum.utils import token_normalized_value
@@ -321,10 +325,19 @@ def decode_transaction(
             events = [eth_event]
 
         with self.database.user_write() as write_cursor:
-            self.dbevents.add_history_events(
-                write_cursor=write_cursor,
-                history=events,
-            )
+            if len(events) > 0:
+                self.dbevents.add_history_events(
+                    write_cursor=write_cursor,
+                    history=events,
+                )
+            else:
+                # This is probably a phishing zero value token transfer tx.
+                # Details here: https://github.com/rotki/rotki/issues/5749
+                self.database.add_to_ignored_action_ids(
+                    write_cursor=write_cursor,
+                    action_type=ActionType.EVM_TRANSACTION,
+                    identifiers=[transaction.identifier],
+                )
             write_cursor.execute(
                 'INSERT OR IGNORE INTO evm_tx_mappings(tx_hash, chain_id, value) VALUES(?, ?, ?)',
                 (transaction.tx_hash, self.evm_inquirer.chain_id.serialize_for_db(), HISTORY_MAPPING_STATE_DECODED),  # noqa: E501


Test patch:
diff --git a/rotkehlchen/tests/unit/decoders/test_1inch.py b/rotkehlchen/tests/unit/decoders/test_1inch.py
--- a/rotkehlchen/tests/unit/decoders/test_1inch.py
+++ b/rotkehlchen/tests/unit/decoders/test_1inch.py
@@ -5,7 +5,6 @@
 from rotkehlchen.accounting.structures.types import HistoryEventSubType, HistoryEventType
 from rotkehlchen.chain.ethereum.modules.oneinch.constants import CPT_ONEINCH_V1, CPT_ONEINCH_V2
 from rotkehlchen.chain.ethereum.modules.uniswap.constants import CPT_UNISWAP_V2
-from rotkehlchen.chain.evm.constants import ZERO_ADDRESS
 from rotkehlchen.chain.evm.decoding.constants import CPT_GAS
 from rotkehlchen.constants.assets import A_DAI, A_ETH, A_USDC
 from rotkehlchen.fval import FVal
@@ -66,18 +65,6 @@ def test_1inchv1_swap(database, ethereum_inquirer):
             location_label=ADDY,
             notes=f'Receive 135.959878392183347402 DAI from 1inch-v1 swap in {ADDY}',
             counterparty=CPT_ONEINCH_V1,
-        ), HistoryBaseEntry(
-            event_identifier=tx_hash,
-            sequence_index=102,
-            timestamp=1594500575000,
-            location=Location.ETHEREUM,
-            event_type=HistoryEventType.SPEND,
-            event_subtype=HistoryEventSubType.NONE,
-            asset=A_CHI,
-            balance=Balance(),
-            location_label=ADDY,
-            notes=f'Send 0 CHI from {ADDY} to {ZERO_ADDRESS}',
-            counterparty=ZERO_ADDRESS,
         ), HistoryBaseEntry(
             event_identifier=tx_hash,
             sequence_index=103,
diff --git a/rotkehlchen/tests/unit/decoders/test_curve.py b/rotkehlchen/tests/unit/decoders/test_curve.py
--- a/rotkehlchen/tests/unit/decoders/test_curve.py
+++ b/rotkehlchen/tests/unit/decoders/test_curve.py
@@ -756,10 +756,10 @@ def test_deposit_multiple_tokens(ethereum_transaction_decoder, ethereum_accounts
             location_label=user_address,
             notes='Receive 9.423568821947938716 crvPlain3andSUSD after depositing in curve pool 0xA5407eAE9Ba41422680e2e00537571bcC53efBfD',  # noqa: E501
             counterparty='curve',
-            extra_data={'deposit_events_num': 4},
+            extra_data={'deposit_events_num': 1},
         ), HistoryBaseEntry(
             event_identifier=evmhash,
-            sequence_index=77,
+            sequence_index=80,
             timestamp=TimestampMS(1675186487000),
             location=Location.ETHEREUM,
             event_type=HistoryEventType.DEPOSIT,
@@ -769,42 +769,6 @@ def test_deposit_multiple_tokens(ethereum_transaction_decoder, ethereum_accounts
             location_label=user_address,
             notes='Deposit 10 DAI in curve pool 0xA5407eAE9Ba41422680e2e00537571bcC53efBfD',  # noqa: E501
             counterparty=CPT_CURVE,
-        ), HistoryBaseEntry(
-            event_identifier=evmhash,
-            sequence_index=78,
-            timestamp=TimestampMS(1675186487000),
-            location=Location.ETHEREUM,
-            event_type=HistoryEventType.DEPOSIT,
-            event_subtype=HistoryEventSubType.DEPOSIT_ASSET,
-            asset=A_USDC,
-            balance=Balance(amount=FVal('0')),
-            location_label=user_address,
-            notes='Deposit 0 USDC in curve pool 0xA5407eAE9Ba41422680e2e00537571bcC53efBfD',  # noqa: E501
-            counterparty=CPT_CURVE,
-        ), HistoryBaseEntry(
-            event_identifier=evmhash,
-            sequence_index=79,
-            timestamp=TimestampMS(1675186487000),
-            location=Location.ETHEREUM,
-            event_type=HistoryEventType.DEPOSIT,
-            event_subtype=HistoryEventSubType.DEPOSIT_ASSET,
-            asset=A_USDT,
-            balance=Balance(amount=FVal('0')),
-            location_label=user_address,
-            notes='Deposit 0 USDT in curve pool 0xA5407eAE9Ba41422680e2e00537571bcC53efBfD',  # noqa: E501
-            counterparty=CPT_CURVE,
-        ), HistoryBaseEntry(
-            event_identifier=evmhash,
-            sequence_index=80,
-            timestamp=TimestampMS(1675186487000),
-            location=Location.ETHEREUM,
-            event_type=HistoryEventType.DEPOSIT,
-            event_subtype=HistoryEventSubType.DEPOSIT_ASSET,
-            asset=Asset('eip155:1/erc20:0x57Ab1ec28D129707052df4dF418D58a2D46d5f51'),
-            balance=Balance(amount=FVal('0')),
-            location_label=user_address,
-            notes='Deposit 0 sUSD in curve pool 0xA5407eAE9Ba41422680e2e00537571bcC53efBfD',  # noqa: E501
-            counterparty=CPT_CURVE,
         ),
     ]
     assert events == expected_events
diff --git a/rotkehlchen/tests/unit/decoders/test_liquity.py b/rotkehlchen/tests/unit/decoders/test_liquity.py
--- a/rotkehlchen/tests/unit/decoders/test_liquity.py
+++ b/rotkehlchen/tests/unit/decoders/test_liquity.py
@@ -336,7 +336,6 @@ def test_liquity_trove_remove_eth(database, ethereum_inquirer, eth_transactions)
         transactions=eth_transactions,
     )
     events = decoder.decode_transaction(transaction=transaction, tx_receipt=receipt)
-    assert len(events) == 3
     expected_events = [
         HistoryBaseEntry(
             event_identifier=evmhash,
@@ -365,19 +364,8 @@ def test_liquity_trove_remove_eth(database, ethereum_inquirer, eth_transactions)
             location_label=user_address,
             notes='Withdraw 32 ETH collateral from liquity',
             counterparty='liquity',
-        ), HistoryBaseEntry(
-            event_identifier=evmhash,
-            sequence_index=295,
-            timestamp=TimestampMS(1646375440000),
-            location=Location.ETHEREUM,
-            event_type=HistoryEventType.SPEND,
-            event_subtype=HistoryEventSubType.PAYBACK_DEBT,
-            asset=A_LUSD,
-            balance=Balance(amount=FVal('0'), usd_value=ZERO),
-            location_label=user_address,
-            notes='Return 0 LUSD to liquity',
-            counterparty='liquity',
-        )]
+        ),
+    ]
     assert events == expected_events
 
 
diff --git a/rotkehlchen/tests/unit/decoders/test_main.py b/rotkehlchen/tests/unit/decoders/test_main.py
--- a/rotkehlchen/tests/unit/decoders/test_main.py
+++ b/rotkehlchen/tests/unit/decoders/test_main.py
@@ -4,7 +4,11 @@
 
 from rotkehlchen.accounting.structures.balance import Balance
 from rotkehlchen.accounting.structures.base import HistoryBaseEntry
-from rotkehlchen.accounting.structures.types import HistoryEventSubType, HistoryEventType
+from rotkehlchen.accounting.structures.types import (
+    ActionType,
+    HistoryEventSubType,
+    HistoryEventType,
+)
 from rotkehlchen.chain.ethereum.constants import CPT_KRAKEN
 from rotkehlchen.chain.ethereum.transactions import EthereumTransactions
 from rotkehlchen.chain.evm.constants import GENESIS_HASH, ZERO_ADDRESS
@@ -604,3 +608,25 @@ def test_genesis_transaction_no_address(database, ethereum_inquirer):
             database=database,
             tx_hash=tx_hex,
         )
+
+
+@pytest.mark.vcr()
+@pytest.mark.parametrize('ethereum_accounts', [['0x9531C059098e3d194fF87FebB587aB07B30B1306']])
+def test_phising_zero_transfers(database, ethereum_inquirer):
+    """Checks that zero transfer phishing transactions are marked as ignored."""
+    tx_hex = '0xb45ef1a202a8d9e983cf59129d28f79057969bb822f62e4b7d9f1ac8853d23ed'
+    evmhash = deserialize_evm_tx_hash(tx_hex)
+    events, _ = get_decoded_events_of_transaction(
+        evm_inquirer=ethereum_inquirer,
+        database=database,
+        tx_hash=evmhash,
+    )
+    assert events == []
+
+    with database.conn.read_ctx() as cursor:
+        ignored_actions = database.get_ignored_action_ids(
+            cursor=cursor,
+            action_type=ActionType.EVM_TRANSACTION,
+        )
+
+    assert ignored_actions == {ActionType.EVM_TRANSACTION: [f'{ChainID.ETHEREUM.value}{tx_hex}']}, 'Transaction with only zero transfers should have been marked as ignored'  # noqa: E501
diff --git a/rotkehlchen/tests/unit/decoders/test_makerdao.py b/rotkehlchen/tests/unit/decoders/test_makerdao.py
--- a/rotkehlchen/tests/unit/decoders/test_makerdao.py
+++ b/rotkehlchen/tests/unit/decoders/test_makerdao.py
@@ -3,7 +3,7 @@
 from rotkehlchen.accounting.structures.balance import Balance
 from rotkehlchen.accounting.structures.base import HistoryBaseEntry
 from rotkehlchen.accounting.structures.types import HistoryEventSubType, HistoryEventType
-from rotkehlchen.constants.assets import A_DAI, A_ETH
+from rotkehlchen.constants.assets import A_ETH
 from rotkehlchen.constants.misc import ZERO
 from rotkehlchen.fval import FVal
 from rotkehlchen.tests.utils.ethereum import get_decoded_events_of_transaction
@@ -56,22 +56,5 @@ def test_makerdao_simple_transaction(
             counterparty='makerdao vault',
             identifier=None,
             extra_data={'vault_type': 'ETH-A'},
-        ), HistoryBaseEntry(
-            event_identifier=HexBytes('0x95de47059bcc084ebb8bdd60f48fbcf05619c2af84bf612fdc27a6bbf9b5097e'),  # noqa: E501
-            sequence_index=105,
-            timestamp=1593572988000,
-            location=Location.ETHEREUM,
-            event_type=HistoryEventType.SPEND,
-            event_subtype=HistoryEventSubType.PAYBACK_DEBT,
-            asset=A_DAI,
-            balance=Balance(amount=ZERO, usd_value=ZERO),
-            location_label='0x648aA14e4424e0825A5cE739C8C68610e143FB79',
-            notes='Payback 0 DAI of debt to makerdao vault 9842',
-            counterparty='makerdao vault',
-            identifier=None,
-            extra_data={
-                'vault_address': '0xdE3b5816b51d88C59C341A0EAbB486F5566542c0',
-                'cdp_id': 9842,
-            },
         ),
     ]