> ## Documentation Index
> Fetch the complete documentation index at: https://docs.goldsky.com/llms.txt
> Use this file to discover all available pages before exploring further.

# ERC-20 transfers

> Build a Turbo pipeline that decodes ERC-20 Transfer events across many or all token contracts and streams them into a database table for analytics.

<Warning>
  Mirror is Goldsky's previous-generation pipeline product. If you're building a new pipeline, use [Turbo](/turbo-pipelines/introduction) instead. See the [Mirror vs. Turbo comparison](/mirror-vs-turbo), or the [migration guide](/turbo-pipelines/migrate-from-mirror) if you have an existing Mirror pipeline.
</Warning>

This guide streams every ERC-20 Transfer event into your own database: the base layer for balance tracking, alerting, and analytics.

This guide is part of a series of tutorials on how you can stream transfer data into your data warehouse using Turbo pipelines. Here we will be focusing on ERC-20 Transfers, visit the following guides for other types of transfers:

* [Native Transfers](/turbo-pipelines/guides/token-transfers/native-transfers)
* [ERC-721 Transfers](/turbo-pipelines/guides/token-transfers/ERC-721-transfers)
* [ERC-1155 Transfers](/turbo-pipelines/guides/token-transfers/ERC-1155-transfers)

## What you'll need

1. A Goldsky account and the CLI installed
   <Accordion title="Install Goldsky's CLI and log in">
     1. Install the Goldsky CLI:

        **For macOS/Linux:**

        ```shell theme={"dark"}
        curl https://goldsky.com | sh
        ```

        **For Windows:**

        ```shell theme={"dark"}
        npm install -g @goldskycom/cli
        ```

        <Note>Windows users need to have Node.js and npm installed first. Download from [nodejs.org](https://nodejs.org) if not already installed.</Note>
     2. Log into your Project by running:
        ```shell theme={"dark"}
        goldsky login
        ```
        This opens your browser to sign in (Google, GitHub, SSO, or email). Once you authenticate, the CLI is logged in automatically — there's no API key to copy or paste.
        <Note>On a headless or remote machine (or in CI), create an API key on your [Project Settings](https://app.goldsky.com/dashboard/settings) page and pass it directly with `goldsky login --token <API_KEY>`. Use `goldsky login --no-browser` to print the login URL instead of opening a browser.</Note>
     3. Now that you are logged in, run `goldsky` to get started:
        ```shell theme={"dark"}
        goldsky
        ```
   </Accordion>
2. A basic understanding of the [Turbo product](/turbo-pipelines/introduction)
3. A destination sink to write your data to. In this example, we will use [the PostgreSQL sink](/turbo-pipelines/sinks/postgres)

## Introduction

In order to stream all the ERC-20 Transfers of a chain there are two potential methods available:

1. Use the readily available ERC-20 dataset for the chain you are interested in: this is the easiest and quickest method to get you streaming token transfers into your sink of choice with minimum code.
2. Build the ERC-20 Transfers pipeline from scratch using raw or decoded logs: this method takes more code and time to implement but it's a great way to learn about how you can use decoding functions in case you
   want to build more customized pipelines.

Let's explore both methods below with more detail:

## Using the ERC-20 transfers source dataset

Every EVM chain has its own ERC-20 dataset available for you to use as source in your pipelines. You can check this by running the `goldsky dataset list` command and finding the EVM chain of your choice.
For this example, let's use Ethereum mainnet and create a simple pipeline definition using its ERC-20 dataset that writes the data into a PostgreSQL instance:

```yaml ethereum-erc20-transfers.yaml expandable theme={"dark"}
name: ethereum-erc20-pipeline
resource_size: s

sources:
  ethereum_erc20_transfers:
    type: dataset
    dataset_name: ethereum.erc20_transfers
    version: 1.2.0
    start_at: earliest

transforms: {}

sinks:
  # Postgres sink for the ethereum.erc20_transfers dataset
  postgres_erc20_transfers:
    type: postgres
    from: ethereum_erc20_transfers
    schema: public
    table: ethereum_erc20_transfers
    secret_name: <YOUR_SECRET>
    primary_key: id
```

<Note>
  If you copy and use this configuration file, make sure to update:

  1. Your `secret_name`. If you already [created a secret](/platform/secrets), you can find it via the CLI command `goldsky secret list`.
  2. The schema and table you want the data written to, by default it writes to `public.ethereum_erc20_transfers`.
</Note>

You can start the pipeline by running:

```bash theme={"dark"}
goldsky turbo apply ethereum-erc20-transfers.yaml
```

That's it! You should soon start seeing ERC-20 token transfers in your database.

<Tip>
  You can watch the data flowing through the pipeline in real time with `goldsky turbo inspect ethereum-erc20-pipeline`. See [Live Inspect](/turbo-pipelines/live-inspect) for details.
</Tip>

## Building ERC-20 transfers from scratch using logs

The ERC-20 datasets used as the source above encapsulate all the decoding logic explained in this section.
Read on if you want to see how it's implemented, or to extend or modify this logic yourself.

To build the token transfers pipeline from scratch, use the `raw_logs` dataset for that chain in combination with [decoding functions](/turbo-pipelines/reference/sql-functions#evm--ethereum-functions) using the ABI of a specific ERC-20 contract.

### Building ERC-20 transfers using decoding functions

In this example, we will stream all the `Transfer` events of all the ERC-20 tokens for the [Scroll chain](https://scroll.io/). To that end, we will dynamically fetch the ABI of the USDT token from the Scrollscan API (available [here](https://api.scrollscan.com/api?module=contract\&action=getabi\&address=0xc7d86908ccf644db7c69437d5852cedbc1ad3f69))
and use it to identify all the same events for the tokens in the chain. We have decided to use the ABI of the USDT token contract for this example but any other ERC-20 compliant token would also work.

We need to differentiate ERC-20 token transfers from ERC-721 (NFT) transfers since they have the same event signature in decoded data: `Transfer(address,address,uint256)`.
However, if we look closely at their event definitions we can appreciate that the number of topics differ:

* [ERC-20](https://ethereum.org/en/developers/docs/standards/tokens/erc-20/): `event Transfer(address indexed _from, address indexed _to, uint256 _value)`
* [ERC-721](https://ethereum.org/en/developers/docs/standards/tokens/erc-721/): `event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId)`

ERC-20 Transfer events have three topics (one topic for event signature + 2 topics for the indexed params).
NFTs on the other hand have four topics as they have one more indexed param in the event signature.
We will use this as a filter in our pipeline transform to only stream ERC-20 Transfer events.

Let's now see all these concepts applied in an example pipeline definition:

#### Pipeline definition

```yaml scroll-erc20-transfers.yaml expandable theme={"dark"}
name: scroll-erc20-transfers
resource_size: s

sources:
  my_scroll_mainnet_raw_logs:
    type: dataset
    dataset_name: scroll_mainnet.raw_logs
    version: 1.0.0
    start_at: earliest

transforms:
  # Fetch the ABI from Scrollscan for USDT and decode matching logs
  scroll_decoded:
    type: sql
    primary_key: id
    sql: |
      SELECT
        *,
        _gs_log_decode(
          _gs_fetch_abi('https://api.scrollscan.com/api?module=contract&action=getabi&address=0xc7d86908ccf644db7c69437d5852cedbc1ad3f69&apikey=YOUR_KEY', 'etherscan'),
          topics,
          data
        ) AS decoded
      FROM my_scroll_mainnet_raw_logs
      WHERE topics LIKE '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef%'
        AND SPLIT_INDEX(topics, ',', 3) IS NULL

  # Clean up the previous transform, unnest the values from the decoded struct
  scroll_clean:
    type: sql
    primary_key: id
    sql: |
      SELECT
        *,
        decoded.event_params AS event_params,
        decoded.event_signature AS event_name
      FROM scroll_decoded
      WHERE decoded IS NOT NULL
        AND decoded.event_signature = 'Transfer'

  # Select the transfer columns we want in the database
  scroll_20_transfers:
    type: sql
    primary_key: id
    sql: |
      SELECT
        id,
        lower(address) AS token_id,
        lower(event_params[1]) AS sender,
        lower(event_params[2]) AS recipient,
        event_params[3] AS value,
        event_name,
        block_number,
        block_hash,
        log_index,
        transaction_hash,
        transaction_index
      FROM scroll_clean

sinks:
  # Postgres sink for ERC-20 transfers
  scroll_20_sink:
    type: postgres
    from: scroll_20_transfers
    schema: public
    table: erc20_transfers
    secret_name: <YOUR_SECRET>
    primary_key: id
```

<Note>
  If you copy and use this configuration file, make sure to update:

  1. Your `secret_name`. If you already [created a secret](/platform/secrets), you can find it via the CLI command `goldsky secret list`.
  2. The schema and table you want the data written to, by default it writes to `public.erc20_transfers`.
</Note>

There are three transforms in this pipeline definition which we'll explain how they work:

```sql Transform: scroll_decoded theme={"dark"}
SELECT
  *,
  _gs_log_decode(
      _gs_fetch_abi('https://api.scrollscan.com/api?module=contract&action=getabi&address=0xc7d86908ccf644db7c69437d5852cedbc1ad3f69&apikey=YOUR_KEY', 'etherscan'),
      topics,
      data
  ) AS decoded
  FROM my_scroll_mainnet_raw_logs
  WHERE topics LIKE '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef%'
  AND SPLIT_INDEX(topics, ',', 3) IS NULL
```

As explained in the [decode custom contract events guide](/turbo-pipelines/sources/evm#guide-decode-custom-contract-events) we first make use of the `_gs_fetch_abi` function to get the ABI from Scrollscan and pass it as first argument
to the function `_gs_log_decode` to decode its topics and data. We store the result in a `decoded` struct which we unnest on the next transform.
We also limit the decoding to the relevant events using the topic filter and `SPLIT_INDEX` to only include ERC-20 transfers.

* `topics LIKE '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef%'`: `topics` is a comma separated string. Each value in the string is a hash. The first is the hash of the full event\_signature (including arguments), in our case `Transfer(address,address,uint256)` for ERC-20, which is hashed to `0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef`. We use `LIKE` to only consider the first signature, with a `%` at the end, which acts as a wildcard.
* `SPLIT_INDEX(topics, ',', 3) IS NULL`: as mentioned in the introduction, ERC-20 transfers share the same `event_signature` as ERC-721 transfers. The difference between them is the number of topics associated with the event. ERC-721 transfers have four topics, and ERC-20 transfers have three.

```sql Transform: scroll_clean theme={"dark"}
SELECT
  *,
  decoded.event_params AS event_params,
  decoded.event_signature AS event_name
  FROM scroll_decoded
  WHERE decoded IS NOT NULL
  AND decoded.event_signature = 'Transfer'
```

In this second transform, we take the `event_params` and `event_signature` from the result of the decoding. We then filter the query on:

* `decoded IS NOT NULL`: to leave out potential null results from the decoder
* `decoded.event_signature = 'Transfer'`: the decoder will output the event name as event\_signature, excluding its arguments. We use it to filter only for Transfer events.

```sql Transform: scroll_20_transfers theme={"dark"}
SELECT
  id,
  lower(address) AS token_id,
  lower(event_params[1]) AS sender,
  lower(event_params[2]) AS recipient,
  event_params[3] AS value,
  event_name,
  block_number,
  block_hash,
  log_index,
  transaction_hash,
  transaction_index
  FROM scroll_clean
```

In this last transform we are essentially selecting all the Transfer information we are interested in having in our database.
We've included a number of columns that you may or may not need, the main columns needed for most purposes are: `id`, `address` (if you are syncing multiple contract addresses), `sender`, `recipient`, `token_id`, and `value`.

* `id`: This is the Goldsky provided `id`, it is a string composed of the dataset name, block hash, and log index, which is unique per event, here's an example: `log_0x60eaf5a2ab37c73cf1f3bbd32fc17f2709953192b530d75aadc521111f476d6c_18`
* `lower(address) AS token_id`: We use the `lower` function here to lower-case the address to make using this data simpler downstream, we also rename the column to `token_id` to make it more explicit.
* `lower(event_params[1]) AS sender`: Here we continue to lower-case values for consistency. In this case we're using the first element of the `event_params` array (using a 1-based index), and renaming it to `sender`. Each event parameter maps to an argument to the `event_signature`.
* `lower(event_params[2]) AS recipient`: Like the previous column, we're pulling the second element in the `event_params` array and renaming it to `recipient`.
* `event_params[3] AS value`: We're pulling the third element in the `event_params` array and renaming it to `value` to represent the amount of the token sent in the transfer.

Lastly, we are also adding more block metadata to the query to add context to each transaction:

```
event_name,
block_number,
block_hash,
log_index,
transaction_hash,
transaction_index
```

It's worth mentioning that in this example we are interested in all the ERC-20 Transfer events but if you would like to filter for specific contract addresses you could simply add a `WHERE` filter to this query with the addresses you are interested in, like: `WHERE address IN ('0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D', '0xdac17f958d2ee523a2206206994597c13d831ec7')`

#### Deploying the pipeline

Our last step is to deploy this pipeline and start sinking ERC-20 transfer data into our database. Assuming we are using the same file name for the pipeline configuration as in this example,
we can use the [CLI apply command](/turbo-pipelines/cli-reference#apply-a-pipeline) like this:

```bash theme={"dark"}
goldsky turbo apply scroll-erc20-transfers.yaml
```

After some time, you should see the pipeline start streaming Transfer data into your sink.

<Note>
  Remember that you can always speed up the streaming process by increasing the
  `resource_size` in your pipeline YAML and re-applying it with `goldsky turbo apply`.
</Note>

Here's an example transfer record from our sink:

| id                                                                         | token\_id                                  | sender                                     | recipient                                  | value             | event\_name | block\_number | block\_hash                                                        | log\_index | transaction\_hash                                                  | transaction\_index |
| -------------------------------------------------------------------------- | ------------------------------------------ | ------------------------------------------ | ------------------------------------------ | ----------------- | ----------- | ------------- | ------------------------------------------------------------------ | ---------- | ------------------------------------------------------------------ | ------------------ |
| log\_0x666622ad5c04eb5a335364d9268e24c64d67d005949570061d6c150271b0da12\_2 | 0x5300000000000000000000000000000000000004 | 0xefeb222f8046aaa032c56290416c3192111c0085 | 0x8c5c4595df2b398a16aa39105b07518466db1e5e | 22000000000000006 | Transfer    | 5136          | 0x666622ad5c04eb5a335364d9268e24c64d67d005949570061d6c150271b0da12 | 2          | 0x63097d8bd16e34caacfa812d7b608c29eb9dd261f1b334aa4cfc31a2dab2f271 | 0                  |

We can find this [transaction in Scrollscan](https://scrollscan.com/tx/0x63097d8bd16e34caacfa812d7b608c29eb9dd261f1b334aa4cfc31a2dab2f271). We see that it corresponds to the second internal transfer of Wrapped ETH (WETH):

<img className="block mx-auto" width="450" src="https://mintcdn.com/goldsky-38/djvhUUMseW21frQF/images/mirror/guides/token-transfers/erc20-transaction.png?fit=max&auto=format&n=djvhUUMseW21frQF&q=85&s=f8e34475e840382bf89c71a258e4700f" data-path="images/mirror/guides/token-transfers/erc20-transaction.png" />

This concludes our successful deployment of a Turbo pipeline streaming ERC-20 tokens from Scroll chain into our database using inline decoders. Congrats!

## Conclusion

In this guide, we have learnt how Turbo pipelines simplify streaming ERC-20 Transfer events into your database.

We have first looked into the easy way of achieving this, simply by making use of the readily available ERC-20 dataset of the EVM chain and using it as the source to our pipeline.

We have also deep dived into the standard decoding method using decoding functions, implementing an example on Scroll chain.

With Turbo pipelines, developers gain flexibility and efficiency in integrating blockchain data, opening up new possibilities for applications and insights.

Can't find what you're looking for? Reach out to us at [support@goldsky.com](mailto:support@goldsky.com) for help.


## Related topics

- [ERC-721 transfers](/turbo-pipelines/guides/token-transfers/ERC-721-transfers.md)
- [ERC-1155 transfers](/turbo-pipelines/guides/token-transfers/ERC-1155-transfers.md)
- [Native transfers](/turbo-pipelines/guides/token-transfers/native-transfers.md)
- [Stablecoin transfers](/turbo-pipelines/guides/token-transfers/stablecoin-transfers.md)
- [S2](/turbo-pipelines/sinks/s2.md)
