General: Contracts: Contract Reads - FlipsideCrypto/fsc-evm GitHub Wiki

Contract Reads Pipeline

Summary:

This guide focuses on an essential aspect of blockchain data analysis: modeling function calls that read data from smart contracts. Smart contracts often contain valuable information that isn't directly visible in transaction data. These 'read' functions - such as querying a token's symbol, decimals, or name - provide crucial context that enrich event logs or traces with contract metadata.

Best Practices, Tips & Tricks:

Implementation Steps, Variables & Notes:

Examples, References & Sources:


Read Functions for Curated Models

Summary:

When building curated models for DeFi primitives such as Liquidity Pools, DEX Swaps, Bridging or even NFTs, it's common that contract metadata can only be accessed via a function call on the contract itself, rather than parsed through event logs or traces. And while we have a pipeline for reading token symbols, names and decimals, there's often other data points or metadata that is necessary to collect, such as the token addresses associated with liquidity pools.

Best Practices, Tips & Tricks:

  • Before building a reads model to call functions on a contract, verify that the metadata you are looking for is not already present in the Contracts pipeline, or emitted in events or traces.
  • Contract reads can be resource intensive. It's crucial to test thoroughly to gauge the required frequency of calls and ensure proper incremental logic is in place to prevent re-reading the contract for metadata that's already been collected, or doesn't exist at all.
  • There are various resources for converting function signatures to text and vice versa. Examples include;
    • Flipside's EVM Signature Search: Tool to search for EVM Function and Event signatures.
    • UDF_KECCAK256(STRING): Custom Snowflake function designed to convert the function name (string) to the hexadecimal representation that can be called on the contract.
      • Alternatively, there are legacy UDFs such as UDFS.STREAMLINE.UDF_HEX_ENCODE_FUNCTION(STRING) that may be used, but please use caution as these are no longer maintained and may be deprecated in the future.

Implementation Steps, Variables & Notes:

  1. Determine the Contract Address that the function should be called on. This can be done in a number of ways, but the most straightforward is to research the Contract on a block explorer such as Etherscan. View the Contract tab, then select Code to view the function code or Read Contract for a structured view of available function calls and outputs.

    • Note on Proxy Contracts: Often times a custom proxy implementation contract is deployed for the parent contract that requires reads. In this case, select the Read as Proxy tab, where the implementation contract address is present. Flipside's silver.proxies table is another great resource for determining parent and proxy addresses.
  2. Determine the Block to call the function against. It is recommended to use the MAX(block_number) associated with the contract rather than the MIN(block_number), as often times metadata is not complete at the moment the contract is deployed.

    • Ensure proper incremental logic is added to limit the amount of data being processed each time the model runs.
  3. Create the Function Signatures CTE with two columns, function_sig and function_name and define the values. This CTE will be referenced in the function call.

    • Utilize the Custom Snowflake UDF UTILS.UDF_KECCAK256 or Flipside's EVM Signature Search to convert the function name into the hexadecimal representation.
      • It is not recommended to place this function directly into the DBT model because it can be resource intensive to call the UDF every time the models runs. The most efficient route is to run this query in Snowflake and hardcode the values in the model, unless they are expected to change frequently (not common).
      • The first 10 characters (including 0x) of the string represent the function_signature.
    SELECT 
         UTILS.UDF_KECCAK256('l1CanonicalToken()') AS function_name,
         LEFT(function_name,10) AS function_signature;
    
  4. Create the Inputs CTE by joining the contract_address and block_number with the function_sigs CTE, producing 1 row for every function_signature intended to be called.

    • Produce the DATA portion of the call by concatenating the function_sig with the padded function_input. This is typically set to 64 digits in length.
  5. Prep the Contract Read rpc_request with UTILS.UDF_JSON_RPC_CALL. Please see eth_call RPC Method for more information on how to structure the request.

    utils.udf_json_rpc_call(
             'eth_call',
             [ { 'to': contract_address, 'from': NULL, 'data': DATA }, 
               utils.udf_int_to_hex(block_number) ]
         ) AS rpc_request,
    
  6. Execute the call with live.udf_api. Please see LiveQuery Documentation for more information.

    live.udf_api(
             'POST',
             CONCAT(
                 '{service}',
                 '/',
                 '{Authentication}'
             ),{},
             rpc_request,
             'Vault/prod/base/quicknode/mainnet'
         ) AS read_output,
    
  7. The result data within the read_output object will contain the output of the call. E.g. read_output :data :result :: STRING AS read_result. This is typically in hexadecimal format and must be transformed according to the expected result. Here is one example of how it might be transformed to an address format.

    CONCAT('0x', SUBSTR(read_result, 27, 40)) AS address
    

Examples, References & Sources: