Repository: rotki/rotki

Issue:
Support sDAI for maker DSR
## Abstract

We already support the very old DSR protocol but we should also support the sDAI way of using the new DSR which has been giving for some time now a good 5% return on DAI.

Known UI is here: https://app-sparkprotocol-io.ipns.dweb.link/sdai/

A big problem here seems to be that deposit/withdraw does not seem to create Transfer events for sDAI so they need to be handled properly in a decoder.


## Specification

- [ ] Create decoder for sDAI actions
- [ ] Properly account for sDAI in accounting
- [ ] Handle balance better maybe. Currently I personally have 1sDAI == 1DAI in my rotki but that is wrong as sDAI is constantly gaining interest and that is shown in the UI above but not in rotki. Probably we can query the contract to get it.



Discussion:

Resolving patch:
diff --git a/rotkehlchen/chain/ethereum/modules/makerdao/constants.py b/rotkehlchen/chain/ethereum/modules/makerdao/constants.py
--- a/rotkehlchen/chain/ethereum/modules/makerdao/constants.py
+++ b/rotkehlchen/chain/ethereum/modules/makerdao/constants.py
@@ -10,6 +10,9 @@
 CPT_VAULT = 'makerdao vault'
 CPT_DSR = 'makerdao dsr'
 CPT_MIGRATION = 'makerdao migration'
+CPT_SDAI = 'sDAI'
 
 MAKERDAO_LABEL = 'Makerdao'
 MAKERDAO_ICON = 'makerdao.svg'
+SDAI_LABEL = 'sDAI contract'
+SDAI_ICON = 'sdai.svg'
diff --git a/rotkehlchen/chain/ethereum/modules/makerdao/decoder.py b/rotkehlchen/chain/ethereum/modules/makerdao/decoder.py
--- a/rotkehlchen/chain/ethereum/modules/makerdao/decoder.py
+++ b/rotkehlchen/chain/ethereum/modules/makerdao/decoder.py
@@ -14,8 +14,10 @@
     DecoderContext,
     DecodingOutput,
 )
-from rotkehlchen.chain.evm.decoding.types import CounterpartyDetails
+from rotkehlchen.chain.evm.decoding.types import CounterpartyDetails, EventCategory
+from rotkehlchen.chain.evm.decoding.utils import maybe_reshuffle_events
 from rotkehlchen.chain.evm.types import string_to_evm_address
+from rotkehlchen.constants import ZERO
 from rotkehlchen.constants.assets import (
     A_AAVE,
     A_BAL,
@@ -32,6 +34,7 @@
     A_PAX,
     A_RENBTC,
     A_SAI,
+    A_SDAI,
     A_TUSD,
     A_UNI,
     A_USDC,
@@ -43,10 +46,19 @@
 from rotkehlchen.errors.asset import UnknownAsset, WrongAssetType
 from rotkehlchen.errors.serialization import DeserializationError
 from rotkehlchen.serialization.deserialize import deserialize_evm_address
-from rotkehlchen.types import ChecksumEvmAddress
+from rotkehlchen.types import ChecksumEvmAddress, DecoderEventMappingType
 from rotkehlchen.utils.misc import hex_or_bytes_to_address, hex_or_bytes_to_int, shift_num_right_by
 
-from .constants import CPT_DSR, CPT_MIGRATION, CPT_VAULT, MAKERDAO_ICON, MAKERDAO_LABEL
+from .constants import (
+    CPT_DSR,
+    CPT_MIGRATION,
+    CPT_SDAI,
+    CPT_VAULT,
+    MAKERDAO_ICON,
+    MAKERDAO_LABEL,
+    SDAI_ICON,
+    SDAI_LABEL,
+)
 
 if TYPE_CHECKING:
     from rotkehlchen.chain.ethereum.node_inquirer import EthereumInquirer
@@ -64,6 +76,9 @@
 CDPMANAGER_MOVE = b'\xf9\xf3\r\xb6\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'  # noqa: E501
 CDPMANAGER_FROB = b'E\xe6\xbd\xcd\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'  # noqa: E501
 
+SDAI_DEPOSIT = b'\xdc\xbc\x1c\x05$\x0f1\xff:\xd0g\xef\x1e\xe3\\\xe4\x99wbu.:\tR\x84uED\xf4\xc7\t\xd7'  # noqa: E501
+SDAI_REDEEM = b'\xfb\xdey} \x1ch\x1b\x91\x05e)\x11\x9e\x0b\x02@|{\xb9jJ,u\xc0\x1f\xc9fr2\xc8\xdb'
+
 
 class MakerdaoDecoder(DecoderInterface, HasDSProxy):
     def __init__(  # pylint: disable=super-init-not-called
@@ -88,6 +103,7 @@ def __init__(  # pylint: disable=super-init-not-called
         )
         self.dai = A_DAI.resolve_to_evm_token()
         self.sai = A_SAI.resolve_to_evm_token()
+        self.sdai = A_SDAI.resolve_to_evm_token()
         self.makerdao_cdp_manager = self.ethereum.contracts.contract(string_to_evm_address('0x5ef30b9986345249bc32d8928B7ee64DE9435E39'))  # noqa: E501
         self.makerdao_dai_join = self.ethereum.contracts.contract(string_to_evm_address('0x9759A6Ac90977b93B58547b4A71c78317f391A28'))  # noqa: E501
 
@@ -436,6 +452,66 @@ def decode_cdp_manager_events(self, context: DecoderContext) -> DecodingOutput:
 
         return DEFAULT_DECODING_OUTPUT
 
+    def decode_sdai_events(self, context: DecoderContext) -> DecodingOutput:
+        if context.tx_log.topics[0] == SDAI_DEPOSIT:  # DAI -> SDAI: owner receives sdai
+            owner_address = self._get_address_or_proxy(hex_or_bytes_to_address(context.tx_log.topics[2]))  # noqa: E501
+            to_address = owner_address
+            from_address: Optional[ChecksumEvmAddress] = self.sdai.evm_address
+        elif context.tx_log.topics[0] == SDAI_REDEEM:  # SDAI -> DAI: owner deposits sdai
+            owner_address = self._get_address_or_proxy(hex_or_bytes_to_address(context.tx_log.topics[3]))  # noqa: E501
+            from_address = owner_address
+            to_address = self.sdai.evm_address
+        else:
+            return DEFAULT_DECODING_OUTPUT
+
+        if from_address is None or to_address is None:
+            return DEFAULT_DECODING_OUTPUT
+
+        amount = asset_normalized_value(
+            amount=hex_or_bytes_to_int(context.tx_log.data[32:64]),
+            asset=self.sdai,
+        )
+        if amount == ZERO:
+            return DEFAULT_DECODING_OUTPUT
+
+        if owner_address == from_address:
+            event_type = HistoryEventType.DEPOSIT
+            event_subtype = HistoryEventSubType.DEPOSIT_ASSET
+            notes = f'Return {amount} sDAI to sDAI contract'
+        else:
+            event_type = HistoryEventType.WITHDRAWAL
+            event_subtype = HistoryEventSubType.REMOVE_ASSET
+            notes = f'Withdraw {amount} sDAI from sDAI contract'
+
+        transfer = self.base.make_event_from_transaction(
+            transaction=context.transaction,
+            tx_log=context.tx_log,
+            event_type=event_type,
+            event_subtype=event_subtype,
+            asset=self.sdai,
+            balance=Balance(amount=amount),
+            location_label=owner_address,
+            notes=notes,
+            address=self.sdai.evm_address,
+            counterparty=CPT_SDAI,
+        )
+
+        for event in context.decoded_events:
+            if event.event_subtype == HistoryEventSubType.NONE:
+                incoming_event = event.event_type == HistoryEventType.RECEIVE
+                event.event_type = HistoryEventType.WITHDRAWAL if incoming_event else HistoryEventType.DEPOSIT  # noqa: E501
+                event.event_subtype = HistoryEventSubType.REMOVE_ASSET if incoming_event else HistoryEventSubType.DEPOSIT_ASSET  # noqa: E501
+                verb, preposition = ('Withdraw', 'from') if incoming_event else ('Deposit', 'to')
+                event.notes = f'{verb} {event.balance.amount} DAI {preposition} sDAI contract'
+                event.address = self.sdai.evm_address
+                event.counterparty = CPT_SDAI
+                deposit_event, receive_event = (transfer, event) if incoming_event else (event, transfer)  # noqa: E501
+                maybe_reshuffle_events(
+                    ordered_events=[deposit_event, receive_event],
+                    events_list=context.decoded_events,
+                )
+        return DecodingOutput(event=transfer, action_items=[])
+
     def decode_saidai_migration(self, context: DecoderContext) -> DecodingOutput:
         if context.tx_log.topics[0] == ERC20_OR_ERC721_TRANSFER:
             to_address = hex_or_bytes_to_address(context.tx_log.topics[2])
@@ -477,6 +553,16 @@ def decode_saidai_migration(self, context: DecoderContext) -> DecodingOutput:
 
     # -- DecoderInterface methods
 
+    def possible_events(self) -> DecoderEventMappingType:
+        return {CPT_SDAI: {
+            HistoryEventType.DEPOSIT: {
+                HistoryEventSubType.DEPOSIT_ASSET: EventCategory.DEPOSIT,
+            },
+            HistoryEventType.WITHDRAWAL: {
+                HistoryEventSubType.REMOVE_ASSET: EventCategory.WITHDRAW,
+            },
+        }}
+
     def addresses_to_decoders(self) -> dict[ChecksumEvmAddress, tuple[Any, ...]]:
         return {
             string_to_evm_address('0x3D0B1912B66114d4096F48A8CEe3A56C231772cA'): (self.decode_makerdao_vault_action, A_BAT.resolve_to_crypto_asset(), 'BAT-A'),  # noqa: E501
@@ -509,6 +595,7 @@ def addresses_to_decoders(self) -> dict[ChecksumEvmAddress, tuple[Any, ...]]:
             string_to_evm_address('0xA26e15C895EFc0616177B7c1e7270A4C7D51C997'): (self.decode_proxy_creation,),  # noqa: E501
             string_to_evm_address('0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359'): (self.decode_saidai_migration,),  # noqa: E501
             self.makerdao_cdp_manager.address: (self.decode_cdp_manager_events,),
+            self.sdai.evm_address: (self.decode_sdai_events,),
         }
 
     def counterparties(self) -> list[CounterpartyDetails]:
@@ -525,5 +612,9 @@ def counterparties(self) -> list[CounterpartyDetails]:
                 identifier=CPT_MIGRATION,
                 label=MAKERDAO_LABEL,
                 image=MAKERDAO_ICON,
+            ), CounterpartyDetails(
+                identifier=CPT_SDAI,
+                label=SDAI_LABEL,
+                image=SDAI_ICON,
             ),
         ]
diff --git a/rotkehlchen/constants/assets.py b/rotkehlchen/constants/assets.py
--- a/rotkehlchen/constants/assets.py
+++ b/rotkehlchen/constants/assets.py
@@ -25,6 +25,7 @@
 A_UNI = Asset('eip155:1/erc20:0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
 A_1INCH = Asset('eip155:1/erc20:0x111111111117dC0aa78b770fA6A738034120C302')
 A_DAI = Asset('eip155:1/erc20:0x6B175474E89094C44Da98b954EedeAC495271d0F')
+A_SDAI = Asset('eip155:1/erc20:0x83F20F44975D03b1b09e64809B757c47f942BEeA')
 A_SAI = Asset('eip155:1/erc20:0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359')
 A_YFI = Asset('eip155:1/erc20:0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e')
 A_PETH = Asset('eip155:1/erc20:0xf53AD2c6851052A81B42133467480961B2321C09')


Test patch:
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
@@ -137,6 +137,7 @@ def test_decoders_initialization(ethereum_transaction_decoder: 'EthereumTransact
         'diva',
         'arbitrum_one',
         'base',
+        'sDAI',
     }
 
 
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,14 +3,13 @@
 from rotkehlchen.accounting.structures.balance import Balance
 from rotkehlchen.accounting.structures.evm_event import EvmEvent
 from rotkehlchen.accounting.structures.types import HistoryEventSubType, HistoryEventType
-from rotkehlchen.chain.ethereum.modules.makerdao.constants import CPT_VAULT
+from rotkehlchen.chain.ethereum.modules.makerdao.constants import CPT_SDAI, CPT_VAULT
 from rotkehlchen.chain.evm.decoding.constants import CPT_GAS
 from rotkehlchen.chain.evm.types import string_to_evm_address
-from rotkehlchen.constants import ZERO
-from rotkehlchen.constants.assets import A_ETH
+from rotkehlchen.constants.assets import A_DAI, A_ETH, A_SDAI
 from rotkehlchen.fval import FVal
 from rotkehlchen.tests.utils.ethereum import get_decoded_events_of_transaction
-from rotkehlchen.types import Location, deserialize_evm_tx_hash
+from rotkehlchen.types import Location, TimestampMS, deserialize_evm_tx_hash
 
 
 @pytest.mark.vcr()
@@ -23,7 +22,7 @@ def test_makerdao_simple_transaction(
     https://etherscan.io/tx/0x95de47059bcc084ebb8bdd60f48fbcf05619c2af84bf612fdc27a6bbf9b5097e
     """
     tx_hash = deserialize_evm_tx_hash('0x95de47059bcc084ebb8bdd60f48fbcf05619c2af84bf612fdc27a6bbf9b5097e')  # noqa: E501
-    # We don't need any events here, we just check that no errors occure during decoding
+    # We don't need any events here, we just check that no errors occured during decoding
     events, _ = get_decoded_events_of_transaction(
         evm_inquirer=ethereum_inquirer,
         database=database,
@@ -38,12 +37,10 @@ def test_makerdao_simple_transaction(
             event_type=HistoryEventType.SPEND,
             event_subtype=HistoryEventSubType.FEE,
             asset=A_ETH,
-            balance=Balance(amount=FVal(0.00926134), usd_value=ZERO),
+            balance=Balance(amount=FVal(0.00926134)),
             location_label='0x648aA14e4424e0825A5cE739C8C68610e143FB79',
             notes='Burned 0.00926134 ETH for gas',
             counterparty=CPT_GAS,
-            identifier=None,
-            extra_data=None,
         ), EvmEvent(
             tx_hash=tx_hash,
             sequence_index=1,
@@ -52,12 +49,129 @@ def test_makerdao_simple_transaction(
             event_type=HistoryEventType.WITHDRAWAL,
             event_subtype=HistoryEventSubType.REMOVE_ASSET,
             asset=A_ETH,
-            balance=Balance(amount=FVal(0.6), usd_value=ZERO),
+            balance=Balance(amount=FVal(0.6)),
             location_label='0x648aA14e4424e0825A5cE739C8C68610e143FB79',
             notes='Withdraw 0.6 ETH from ETH-A MakerDAO vault',
             counterparty=CPT_VAULT,
             address=string_to_evm_address('0x809aade1B623d8cDAeb86484d3366A03F8841FBc'),
-            identifier=None,
             extra_data={'vault_type': 'ETH-A'},
         ),
     ]
+
+
+@pytest.mark.vcr()
+@pytest.mark.parametrize('ethereum_accounts', [['0xa217BDa86b0EDb86eE7d4D6e34F493eDF1ea4F29']])  # noqa: E501
+def test_withdraw_dai_from_sdai(
+        database,
+        ethereum_inquirer,
+        ethereum_accounts,
+):
+    tx_hash = deserialize_evm_tx_hash('0x6b2a1f836cfc7c28002e4ac60297daa6d79fcde892d9c3b9ca723dea2f21af5c')  # noqa: E501
+    events, _ = get_decoded_events_of_transaction(
+        evm_inquirer=ethereum_inquirer,
+        database=database,
+        tx_hash=tx_hash,
+    )
+    user_address = ethereum_accounts[0]
+    timestamp = TimestampMS(1695854591000)
+    sdai = A_SDAI.resolve_to_evm_token()
+    assert events == [
+        EvmEvent(
+            sequence_index=0,
+            timestamp=timestamp,
+            location=Location.ETHEREUM,
+            event_type=HistoryEventType.SPEND,
+            event_subtype=HistoryEventSubType.FEE,
+            asset=A_ETH,
+            balance=Balance(amount=FVal('0.001301015216220134')),
+            location_label=user_address,
+            notes='Burned 0.001301015216220134 ETH for gas',
+            tx_hash=tx_hash,
+            counterparty=CPT_GAS,
+        ), EvmEvent(
+            sequence_index=1,
+            timestamp=timestamp,
+            location=Location.ETHEREUM,
+            event_type=HistoryEventType.DEPOSIT,
+            event_subtype=HistoryEventSubType.DEPOSIT_ASSET,
+            asset=A_SDAI,
+            balance=Balance(amount=FVal('16.020774067834506624')),
+            location_label=user_address,
+            notes='Return 16.020774067834506624 sDAI to sDAI contract',
+            tx_hash=tx_hash,
+            counterparty=CPT_SDAI,
+            address=sdai.evm_address,
+        ), EvmEvent(
+            sequence_index=2,
+            timestamp=timestamp,
+            location=Location.ETHEREUM,
+            event_type=HistoryEventType.WITHDRAWAL,
+            event_subtype=HistoryEventSubType.REMOVE_ASSET,
+            asset=A_DAI,
+            balance=Balance(amount=FVal('16.601085935411927527')),
+            location_label=user_address,
+            notes='Withdraw 16.601085935411927527 DAI from sDAI contract',
+            tx_hash=tx_hash,
+            counterparty=CPT_SDAI,
+            address=sdai.evm_address,
+        ),
+    ]
+
+
+@pytest.mark.vcr()
+@pytest.mark.parametrize('ethereum_accounts', [['0xa217BDa86b0EDb86eE7d4D6e34F493eDF1ea4F29']])  # noqa: E501
+def test_deposit_dai_to_sdai(
+        database,
+        ethereum_inquirer,
+        ethereum_accounts,
+):
+    user_address = ethereum_accounts[0]
+    tx_hash = deserialize_evm_tx_hash('0x27bd72a2ccd999a44c2a7aaed9090572f34045d62e153362a34715a70ca7a6a7')  # noqa: E501
+    events, _ = get_decoded_events_of_transaction(
+        evm_inquirer=ethereum_inquirer,
+        database=database,
+        tx_hash=tx_hash,
+    )
+    timestamp = TimestampMS(1695089927000)
+    address = A_SDAI.resolve_to_evm_token().evm_address
+    assert events == [
+        EvmEvent(
+            sequence_index=0,
+            timestamp=timestamp,
+            location=Location.ETHEREUM,
+            event_type=HistoryEventType.SPEND,
+            event_subtype=HistoryEventSubType.FEE,
+            asset=A_ETH,
+            balance=Balance(amount=FVal(0.00152049387145495)),
+            location_label=user_address,
+            notes='Burned 0.00152049387145495 ETH for gas',
+            tx_hash=tx_hash,
+            counterparty=CPT_GAS,
+        ), EvmEvent(
+            sequence_index=1,
+            timestamp=timestamp,
+            location=Location.ETHEREUM,
+            event_type=HistoryEventType.DEPOSIT,
+            event_subtype=HistoryEventSubType.DEPOSIT_ASSET,
+            asset=A_DAI,
+            balance=Balance(amount=FVal(16.58145794)),
+            location_label=user_address,
+            notes='Deposit 16.58145794 DAI to sDAI contract',
+            tx_hash=tx_hash,
+            counterparty=CPT_SDAI,
+            address=address,
+        ), EvmEvent(
+            sequence_index=2,
+            timestamp=timestamp,
+            location=Location.ETHEREUM,
+            event_type=HistoryEventType.WITHDRAWAL,
+            event_subtype=HistoryEventSubType.REMOVE_ASSET,
+            asset=A_SDAI,
+            balance=Balance(amount=FVal('16.020774067834506624')),
+            location_label=user_address,
+            notes='Withdraw 16.020774067834506624 sDAI from sDAI contract',
+            tx_hash=tx_hash,
+            counterparty=CPT_SDAI,
+            address=address,
+        ),
+    ]