Skip to content

Inconsistent method decoding in data-decoder endpoint across networks endpoint /api/v1/data-decoder #2508

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
alfredolopez80 opened this issue Apr 22, 2025 · 0 comments · May be fixed by #2509
Labels
bug Something isn't working

Comments

@alfredolopez80
Copy link

alfredolopez80 commented Apr 22, 2025

Inconsistent method decoding in data-decoder endpoint across networks

Issue Description

The /api/v1/data-decoder endpoint shows inconsistent behavior when decoding methods across different networks (Base, Polygon, Mainnet). Methods are not being decoded even when:

  1. The contracts are verified and available in the /api/v1/contracts/{address} endpoint
  2. Their ABIs are loaded in the transaction indexer
  3. The contracts are not part of the hardcoded ABIs (/contracts/decoder_abis/)

Steps to Reproduce

Mainnet

URL: https://safe-transaction-mainnet.safe.global/
Contract: 0xB4EFd85c19999D84251304bDA99E90B92300Bd93

# Verify contract is loaded with ABI
curl 'https://safe-transaction-mainnet.safe.global/api/v1/contracts/0xB4EFd85c19999D84251304bDA99E90B92300Bd93/'

# Method: setSaleContractFinalised (not in decoder_abis)
curl -X POST 'https://safe-transaction-mainnet.safe.global/api/v1/data-decoder/' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{"data": "0x4a5c5e2e000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045", "to": "0xB4EFd85c19999D84251304bDA99E90B92300Bd93"}'

# Actual Response : {"method":"fallback","parameters":[]}
# Expected Response: 
{
  "method": "setSaleContractFinalised",
  "parameters": [
    {
      "name": "_sender",
      "type": "address",
      "value": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045"
    }
  ]
}

Polygon

URL: https://safe-transaction-polygon.safe.global/
Contract: 0x062FfE63b7A0d7f27A8105e717c6Ea45E5848AD3

# Verify contract is loaded with ABI
curl 'https://safe-transaction-polygon.safe.global/api/v1/contracts/0x062FfE63b7A0d7f27A8105e717c6Ea45E5848AD3/'

# Method: add (not in decoder_abis)
curl -X POST 'https://safe-transaction-polygon.safe.global/api/v1/data-decoder/' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{"data": "0x0a97563f0000000000000000000000000000000000000000000000000000000000000064000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045000000000000000000000000b4efd85c19999d84251304bda99e90b92300bd93", "to": "0x062FfE63b7A0d7f27A8105e717c6Ea45E5848AD3"}'

# Response: {"method":"fallback","parameters":[]}
# Expected Response:
{
  "method": "add",
  "parameters": [
    {
      "name": "allocPoint",
      "type": "uint256",
      "value": "100"
    },
    {
      "name": "_lpToken",
      "type": "address",
      "value": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045"
    },
    {
      "name": "_rewarder",
      "type": "address",
      "value": "0xb4efd85c19999d84251304bda99e90b92300bd93"
    }
  ]
}

Base

URL: https://safe-transaction-base.safe.global/
Contract: 0x2C8bC3198903DE6b61C1E1aA449578863Af3ccE7

# Verify contract is loaded with ABI
curl 'https://safe-transaction-base.safe.global/api/v1/contracts/0x2C8bC3198903DE6b61C1E1aA449578863Af3ccE7/'

# Method: initialize (not in decoder_abis)
curl -X POST 'https://safe-transaction-base.safe.global/api/v1/data-decoder/' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{"data": "0xc4d66de8000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045", "to": "0x2C8bC3198903DE6b61C1E1aA449578863Af3ccE7"}'

# Response: {"method":"fallback","parameters":[]}
# Expected Response:
{
  "method": "initialize",
  "parameters": [
    {
      "name": "admin",
      "type": "address",
      "value": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045"
    }
  ]
}

Verification Steps

  1. All contracts can be verified to have their ABIs loaded by checking:

  2. None of these methods are present in the hardcoded ABIs in decoder_abis folder:

- aave.py
- admin_upgradeability_proxy.py
- balancer.py
- chainlink.py
- compound.py
- gnosis_protocol.py
- gnosis_safe.py
- idle.py
- maker_dao.py
- open_zeppelin.py
- request.py
- sablier.py
- sight.py
- snapshot.py
- timelock.py

Expected Behavior

The endpoint should be able to decode methods from contracts that:

  1. Are verified in the transaction indexer
  2. Have their ABIs loaded and available through the /api/v1/contracts/{address} endpoint
  3. Even if they're not part of the hardcoded ABIs in decoder_abis

Actual Behavior

  • Only decodes methods that are part of the hardcoded ABIs in decoder_abis
  • Returns fallback for any method not in these ABIs
  • Ignores available ABIs from verified contracts in the transaction indexer
  • This limitation exists across all networks (Base, Polygon, Mainnet)

Suggested Solutions

Technical Analysis of the Transaction Decoder Issue

Class Hierarchy and Initialization

After analyzing the codebase, I've identified the root cause of the issue is in the transaction decoding architecture. There's a class inheritance structure:

SafeTxDecoder (base class)
    ↓
TxDecoder (adds multisend capability)
    ↓
DbTxDecoder (adds database ABI loading)

Initialization Flow

  1. SafeTxDecoder (Base Class):

    • Initializes with a method __init__ that loads ABIs into self.fn_selectors_with_abis
    • Gets supported ABIs from get_supported_abis() which only returns hardcoded Safe contract ABIs
    • This dictionary maps function selectors (first 4 bytes of calldata) to ABIs
  2. TxDecoder (Middle Class):

    • Extends SafeTxDecoder
    • Overrides get_supported_abis() to include additional hardcoded ABIs (ERC20, etc.)
    • Adds multisend decoding capability
  3. DbTxDecoder (Top Class):

    • Extends TxDecoder
    • Overrides get_supported_abis() to include ABIs from the database
    • Adds a method get_contract_abi() to fetch ABIs for specific contracts
    • Overrides get_abi_function() to attempt to use contract-specific ABIs when available

The Critical Issue

The problem occurs in the get_abi_function method in DbTxDecoder:

def get_abi_function(self, data: bytes, address: Optional[ChecksumAddress] = None) -> Optional[ABIFunction]:
    selector = data[:4]
    # Check first that selector is supported in our database
    if selector in self.fn_selectors_with_abis:
        # Try to use specific ABI if address provided
        if address:
            contract_selectors_with_abis = self.get_contract_abi(address)
            if contract_selectors_with_abis and selector in contract_selectors_with_abis:
                # If the selector is available in the abi specific for the address we will use that one
                return contract_selectors_with_abis[selector]
        return self.fn_selectors_with_abis[selector]

The fundamental problem is:

  1. The function first checks if the selector exists in self.fn_selectors_with_abis
  2. This dictionary is populated during initialization with ABIs from get_supported_abis()
  3. Despite DbTxDecoder overriding get_supported_abis() to include database ABIs, it only loads the ABIs present in the database at service startup time
  4. The critical limitation: If a contract ABI is added to the database after the service starts, or if a contract selector wasn't included in the initial loading, it will NEVER be used even if:
    • The contract is verified in the database
    • A valid ABI exists for that contract in the database
    • The API can serve that ABI through /api/v1/contracts/{address}/

Detailed Flow Explanation

  1. When a request hits the /api/v1/data-decoder/ endpoint, DataDecoderView processes it
  2. The view calls get_data_decoded_from_data()
  3. This function gets a DbTxDecoder instance via get_db_tx_decoder()
  4. The decoder attempts to decode the data using get_data_decoded()
  5. Inside this method, it calls decode_transaction_with_types() which uses get_abi_function()
  6. get_abi_function() first checks if the selector exists in the preloaded ABIs before attempting to fetch the contract-specific ABI
  7. If the selector doesn't exist in preloaded ABIs, it returns None and decoding fails, returning a fallback response

The Specific Bug

The implementation creates a "gatekeeper" pattern where get_abi_function() checks if a selector exists in the preloaded pool before attempting to use contract-specific ABIs. This has two key problems:

  1. It doesn't attempt to use contract-specific ABIs unless the selector is already known
  2. It effectively ignores all verified contracts with ABIs in the database that weren't present when the service started

This design prevents the service from utilizing the complete set of ABIs available in the system, particularly for any contract added after service initialization or for any method that isn't part of the common hardcoded ABIs in the /decoder_abis/ directory.

Impact

This limitation affects the usability of the Safe Transaction Service, especially when:

  • Interacting with newer or custom contracts
  • Working with contracts that aren't part of the standard protocols in decoder_abis
  • Trying to decode methods from verified contracts that should be available in the indexer
@alfredolopez80 alfredolopez80 added the bug Something isn't working label Apr 22, 2025
alfredolopez80 added a commit to alfredolopez80/safe-transaction-service that referenced this issue Apr 22, 2025
…e Inconsistent method decoding in data-decoder endpoint across networks endpoint /api/v1/data-decoder safe-global#2508
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
1 participant