fix: Filter WETH transfers in indexer + migration to delete historical incorrect WETH transfers (#10134)

pull/10203/head
nikitosing 6 months ago committed by GitHub
parent c77180c3ac
commit 80a8e3b464
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      apps/explorer/config/config.exs
  2. 1
      apps/explorer/config/runtime/test.exs
  3. 1
      apps/explorer/lib/explorer/application.ex
  4. 12
      apps/explorer/lib/explorer/chain/token_transfer.ex
  5. 145
      apps/explorer/lib/explorer/migrator/sanitize_incorrect_weth_token_transfers.ex
  6. 161
      apps/explorer/test/explorer/migrator/sanitize_incorrect_weth_token_transfers_test.exs
  7. 33
      apps/indexer/lib/indexer/transform/token_transfers.ex
  8. 153
      apps/indexer/test/indexer/transform/token_transfers_test.exs
  9. 7
      config/runtime.exs

@ -122,6 +122,7 @@ config :explorer, Explorer.Migrator.AddressTokenBalanceTokenType, enabled: true
config :explorer, Explorer.Migrator.SanitizeMissingBlockRanges, enabled: true
config :explorer, Explorer.Migrator.SanitizeIncorrectNFTTokenTransfers, enabled: true
config :explorer, Explorer.Migrator.TokenTransferTokenType, enabled: true
config :explorer, Explorer.Migrator.SanitizeIncorrectWETHTokenTransfers, enabled: true
config :explorer, Explorer.Chain.Fetcher.CheckBytecodeMatchingOnDemand, enabled: true

@ -44,6 +44,7 @@ config :explorer, Explorer.Migrator.AddressTokenBalanceTokenType, enabled: false
config :explorer, Explorer.Migrator.SanitizeMissingBlockRanges, enabled: false
config :explorer, Explorer.Migrator.SanitizeIncorrectNFTTokenTransfers, enabled: false
config :explorer, Explorer.Migrator.TokenTransferTokenType, enabled: false
config :explorer, Explorer.Migrator.SanitizeIncorrectWETHTokenTransfers, enabled: false
config :explorer,
realtime_events_sender: Explorer.Chain.Events.SimpleSender

@ -137,6 +137,7 @@ defmodule Explorer.Application do
configure(Explorer.Migrator.SanitizeMissingBlockRanges),
configure(Explorer.Migrator.SanitizeIncorrectNFTTokenTransfers),
configure(Explorer.Migrator.TokenTransferTokenType),
configure(Explorer.Migrator.SanitizeIncorrectWETHTokenTransfers),
configure_chain_type_dependent_process(Explorer.Chain.Cache.StabilityValidatorsCounters, :stability)
]
|> List.flatten()

@ -528,4 +528,16 @@ defmodule Explorer.Chain.TokenTransfer do
defp logs_to_token_transfers_query(query, []) do
query
end
@doc """
Checks if `WHITELISTED_WETH_CONTRACTS` env contains provided address hash.
WHITELISTED_WETH_CONTRACTS env is the list of whitelisted WETH contracts addresses.
"""
@spec whitelisted_weth_contract?(any()) :: boolean()
def whitelisted_weth_contract?(contract_address_hash),
do:
(contract_address_hash |> to_string() |> String.downcase()) in Application.get_env(
:explorer,
Explorer.Chain.TokenTransfer
)[:whitelisted_weth_contracts]
end

@ -0,0 +1,145 @@
defmodule Explorer.Migrator.SanitizeIncorrectWETHTokenTransfers do
@moduledoc """
This migrator will delete all incorrect WETH token transfers. As incorrect we consider:
- WETH withdrawals and WETH deposits emitted by tokens which are not in `WHITELISTED_WETH_CONTRACTS` env
- WETH withdrawal or WETH deposit which has sibling token transfer within the same block and transaction, with the same amount, same from and to addresses, same token contract addresses. (We consider such pairs as duplicates)
"""
use GenServer, restart: :transient
import Ecto.Query
require Logger
alias Explorer.Chain.{Log, TokenTransfer}
alias Explorer.Migrator.MigrationStatus
alias Explorer.Repo
@migration_name "sanitize_incorrect_weth_transfers"
@default_batch_size 500
def start_link(_) do
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
end
@impl true
def init(_) do
case MigrationStatus.get_status(@migration_name) do
"completed" ->
:ignore
_ ->
MigrationStatus.set_status(@migration_name, "started")
schedule_batch_migration()
{:ok, %{step: :delete_not_whitelisted_weth_transfers}}
end
end
@impl true
def handle_info(:migrate_batch, %{step: step} = state) do
case last_unprocessed_identifiers(step) do
[] ->
case step do
:delete_not_whitelisted_weth_transfers ->
Logger.info(
"SanitizeIncorrectWETHTokenTransfers deletion of not whitelisted weth transfers finished, continuing with duplicates deletion"
)
schedule_batch_migration()
{:noreply, %{step: :delete_duplicates}}
:delete_duplicates ->
Logger.info("SanitizeIncorrectWETHTokenTransfers migration finished")
MigrationStatus.set_status(@migration_name, "completed")
{:stop, :normal, state}
end
identifiers ->
identifiers
|> Enum.chunk_every(batch_size())
|> Enum.map(&run_task/1)
|> Task.await_many(:infinity)
schedule_batch_migration()
{:noreply, state}
end
end
defp last_unprocessed_identifiers(step) do
limit = batch_size() * concurrency()
step
|> unprocessed_identifiers()
|> limit(^limit)
|> Repo.all(timeout: :infinity)
end
defp unprocessed_identifiers(:delete_duplicates) do
weth_transfers =
from(
tt in TokenTransfer,
left_join: l in Log,
on: tt.block_hash == l.block_hash and tt.transaction_hash == l.transaction_hash and tt.log_index == l.index,
where:
l.first_topic == ^TokenTransfer.weth_deposit_signature() or
l.first_topic == ^TokenTransfer.weth_withdrawal_signature()
)
from(
weth_tt in subquery(weth_transfers),
inner_join: tt in TokenTransfer,
on: weth_tt.block_hash == tt.block_hash and weth_tt.transaction_hash == tt.transaction_hash,
where:
weth_tt.log_index != tt.log_index and weth_tt.token_contract_address_hash == tt.token_contract_address_hash and
weth_tt.to_address_hash == tt.to_address_hash and weth_tt.from_address_hash == tt.from_address_hash and
weth_tt.amount == tt.amount,
select: {weth_tt.transaction_hash, weth_tt.block_hash, weth_tt.log_index}
)
end
defp unprocessed_identifiers(:delete_not_whitelisted_weth_transfers) do
from(
tt in TokenTransfer,
left_join: l in Log,
on: tt.block_hash == l.block_hash and tt.transaction_hash == l.transaction_hash and tt.log_index == l.index,
where:
(l.first_topic == ^TokenTransfer.weth_deposit_signature() or
l.first_topic == ^TokenTransfer.weth_withdrawal_signature()) and
tt.token_contract_address_hash not in ^Application.get_env(:explorer, Explorer.Chain.TokenTransfer)[
:whitelisted_weth_contracts
],
select: {tt.transaction_hash, tt.block_hash, tt.log_index}
)
end
defp run_task(batch), do: Task.async(fn -> handle_batch(batch) end)
defp handle_batch(token_transfer_ids) do
token_transfer_ids
|> build_delete_query()
|> Repo.query!([], timeout: :infinity)
end
defp schedule_batch_migration do
Process.send(self(), :migrate_batch, [])
end
defp batch_size do
Application.get_env(:explorer, __MODULE__)[:batch_size] || @default_batch_size
end
defp concurrency do
default = 4 * System.schedulers_online()
Application.get_env(:explorer, __MODULE__)[:concurrency] || default
end
defp build_delete_query(token_transfer_ids) do
"""
DELETE
FROM token_transfers tt
WHERE (tt.transaction_hash, tt.block_hash, tt.log_index) IN #{TokenTransfer.encode_token_transfer_ids(token_transfer_ids)}
"""
end
end

@ -0,0 +1,161 @@
defmodule Explorer.Migrator.SanitizeIncorrectWETHTokenTransfersTest do
use Explorer.DataCase, async: false
alias Explorer.Chain.TokenTransfer
alias Explorer.Migrator.{SanitizeIncorrectWETHTokenTransfers, MigrationStatus}
alias Explorer.Repo
describe "SanitizeIncorrectWETHTokenTransfers" do
test "Deletes not whitelisted WETH transfers and duplicated WETH transfers" do
%{contract_address: token_address} = insert(:token, type: "ERC-20")
block = insert(:block, consensus: true)
burn_address = insert(:address, hash: "0x0000000000000000000000000000000000000000")
insert(:token_transfer,
from_address: insert(:address),
block: block,
block_number: block.number,
token_contract_address: token_address,
token_ids: nil
)
deposit_log = insert(:log, first_topic: TokenTransfer.weth_deposit_signature())
insert(:token_transfer,
from_address: insert(:address),
token_contract_address: token_address,
block: deposit_log.block,
transaction: deposit_log.transaction,
log_index: deposit_log.index
)
withdrawal_log = insert(:log, first_topic: TokenTransfer.weth_withdrawal_signature())
insert(:token_transfer,
from_address: insert(:address),
token_contract_address: token_address,
block: withdrawal_log.block,
transaction: withdrawal_log.transaction,
log_index: withdrawal_log.index
)
%{contract_address: whitelisted_token_address} = insert(:token, type: "ERC-20")
env = Application.get_env(:explorer, Explorer.Chain.TokenTransfer)
Application.put_env(
:explorer,
Explorer.Chain.TokenTransfer,
Keyword.put(env, :whitelisted_weth_contracts, [whitelisted_token_address |> to_string() |> String.downcase()])
)
withdrawal_log = insert(:log, first_topic: TokenTransfer.weth_withdrawal_signature())
insert(:token_transfer,
from_address: insert(:address),
token_contract_address: whitelisted_token_address,
block: withdrawal_log.block,
transaction: withdrawal_log.transaction,
log_index: withdrawal_log.index
)
deposit_log = insert(:log, first_topic: TokenTransfer.weth_deposit_signature())
insert(:token_transfer,
from_address: insert(:address),
token_contract_address: whitelisted_token_address,
block: deposit_log.block,
transaction: deposit_log.transaction,
log_index: deposit_log.index
)
withdrawal_log_duplicate = insert(:log, first_topic: TokenTransfer.weth_withdrawal_signature())
tt_withdrawal =
insert(:token_transfer,
from_address: burn_address,
token_contract_address: whitelisted_token_address,
block: withdrawal_log_duplicate.block,
transaction: withdrawal_log_duplicate.transaction,
log_index: withdrawal_log_duplicate.index
)
insert(:token_transfer,
from_address: burn_address,
to_address: tt_withdrawal.to_address,
token_contract_address: whitelisted_token_address,
block: withdrawal_log_duplicate.block,
transaction: withdrawal_log_duplicate.transaction,
log_index: withdrawal_log_duplicate.index + 1,
amount: tt_withdrawal.amount
)
deposit_log_duplicate = insert(:log, first_topic: TokenTransfer.weth_deposit_signature())
tt_deposit =
insert(:token_transfer,
to_address: burn_address,
token_contract_address: whitelisted_token_address,
block: deposit_log_duplicate.block,
transaction: deposit_log_duplicate.transaction,
log_index: deposit_log_duplicate.index
)
insert(:token_transfer,
from_address: tt_deposit.from_address,
to_address: burn_address,
token_contract_address: whitelisted_token_address,
block: deposit_log_duplicate.block,
transaction: deposit_log_duplicate.transaction,
log_index: deposit_log_duplicate.index + 1,
amount: tt_deposit.amount
)
assert MigrationStatus.get_status("sanitize_incorrect_weth_transfers") == nil
Application.put_env(:explorer, Explorer.Migrator.SanitizeIncorrectWETHTokenTransfers,
batch_size: 1,
concurrency: 1
)
SanitizeIncorrectWETHTokenTransfers.start_link([])
Process.sleep(100)
assert MigrationStatus.get_status("sanitize_incorrect_weth_transfers") == "completed"
token_address_hash = token_address.hash
whitelisted_token_address_hash = whitelisted_token_address.hash
assert [
%{token_contract_address_hash: ^token_address_hash},
%{token_contract_address_hash: ^whitelisted_token_address_hash},
%{token_contract_address_hash: ^whitelisted_token_address_hash},
%{token_contract_address_hash: ^whitelisted_token_address_hash},
%{token_contract_address_hash: ^whitelisted_token_address_hash}
] = transfers = Repo.all(TokenTransfer)
withdrawal = Enum.at(transfers, 1)
deposit = Enum.at(transfers, 2)
assert withdrawal.block_hash == withdrawal_log.block_hash
assert withdrawal.transaction_hash == withdrawal_log.transaction_hash
assert withdrawal.log_index == withdrawal_log.index
assert deposit.block_hash == deposit_log.block_hash
assert deposit.transaction_hash == deposit_log.transaction_hash
assert deposit.log_index == deposit_log.index
withdrawal_analogue = Enum.at(transfers, 3)
deposit_analogue = Enum.at(transfers, 4)
assert withdrawal_analogue.block_hash == withdrawal_log_duplicate.block_hash
assert withdrawal_analogue.transaction_hash == withdrawal_log_duplicate.transaction_hash
assert withdrawal_analogue.log_index == withdrawal_log_duplicate.index + 1
assert deposit_analogue.block_hash == deposit_log_duplicate.block_hash
assert deposit_analogue.transaction_hash == deposit_log_duplicate.transaction_hash
assert deposit_analogue.log_index == deposit_log_duplicate.index + 1
Application.put_env(:explorer, Explorer.Chain.TokenTransfer, env)
end
end
end

@ -25,10 +25,12 @@ defmodule Indexer.Transform.TokenTransfers do
weth_transfers =
logs
|> Enum.filter(fn log ->
log.first_topic == TokenTransfer.weth_deposit_signature() ||
log.first_topic == TokenTransfer.weth_withdrawal_signature()
(log.first_topic == TokenTransfer.weth_deposit_signature() ||
log.first_topic == TokenTransfer.weth_withdrawal_signature()) &&
TokenTransfer.whitelisted_weth_contract?(log.address_hash)
end)
|> Enum.reduce(initial_acc, &do_parse/2)
|> drop_repeated_token_transfers(erc20_and_erc721_token_transfers.token_transfers)
erc1155_token_transfers =
logs
@ -80,6 +82,33 @@ defmodule Indexer.Transform.TokenTransfers do
token_transfers_from_logs_uniq
end
defp drop_repeated_token_transfers(weth_acc, erc_20_721_token_transfers) do
key_from_tt = fn tt ->
{tt.block_hash, tt.transaction_hash, tt.token_contract_address_hash, tt.to_address_hash, tt.from_address_hash,
tt.amount}
end
deposit_withdrawal_like_transfers =
Enum.reduce(erc_20_721_token_transfers, %{}, fn token_transfer, acc ->
if token_transfer.token_type == "ERC-20" and
(token_transfer.from_address_hash == burn_address_hash_string() or
token_transfer.to_address_hash == burn_address_hash_string()) do
Map.put(acc, key_from_tt.(token_transfer), true)
else
acc
end
end)
%{token_transfers: weth_token_transfer} = weth_acc
weth_token_transfer_updated =
Enum.reject(weth_token_transfer, fn weth_tt ->
deposit_withdrawal_like_transfers[key_from_tt.(weth_tt)]
end)
Map.put(weth_acc, :token_transfers, weth_token_transfer_updated)
end
defp sanitize_weth_transfers(total_tokens, total_transfers, weth_transfers) do
existing_token_types_map =
total_tokens

@ -137,7 +137,19 @@ defmodule Indexer.Transform.TokenTransfersTest do
]
}
env = Application.get_env(:explorer, Explorer.Chain.TokenTransfer)
Application.put_env(
:explorer,
Explorer.Chain.TokenTransfer,
Keyword.put(env, :whitelisted_weth_contracts, [
weth_deposit_log.address_hash |> to_string() |> String.downcase()
])
)
assert TokenTransfers.parse(logs) == expected
Application.put_env(:explorer, Explorer.Chain.TokenTransfer, env)
end
test "parses ERC-721 transfer with addresses in data field" do
@ -435,6 +447,147 @@ defmodule Indexer.Transform.TokenTransfersTest do
]
}
end
test "Filters WETH transfers from not whitelisted tokens" do
logs = [
%{
address_hash: "0x0BE9e53fd7EDaC9F859882AfdDa116645287C629",
block_number: 23_704_638,
block_hash: "0x8f61c99b0dd1196714ffda5bf979a282e6a62fdd3cff25c291284e6b57de2106",
data: "0x00000000000000000000000000000000000000000000002be19edfcf6b480000",
first_topic: "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c",
second_topic: "0x000000000000000000000000fb76e9e7d88e308ab530330ed90e84a952570319",
third_topic: nil,
fourth_topic: nil,
index: 1,
transaction_hash: "0x185889bc91372106ecf114a4e23f4ee615e131ae3e698078bd5d2ed7e3f55a49"
},
%{
address_hash: "0x0BE9e53fd7EDaC9F859882AfdDa116645287C629",
block_number: 23_704_608,
block_hash: "0x5a5e69984f78d65fc6d92e18058d21a9b114f1d56d06ca7aa017b3d87bf0491a",
data: "0x00000000000000000000000000000000000000000000000000e1315e1ebd28e8",
first_topic: "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65",
second_topic: "0x000000000000000000000000e3f85aad0c8dd7337427b9df5d0fb741d65eeeb5",
third_topic: nil,
fourth_topic: nil,
index: 1,
transaction_hash: "0x07510dbfddbac9064f7d607c2d9a14aa26fa19cdfcd578c0b585ff2395df543f"
}
]
expected = %{token_transfers: [], tokens: []}
assert TokenTransfers.parse(logs) == expected
end
test "Filters duplicates WETH transfers" do
[log_1, _weth_deposit_log, log_2, _weth_withdrawal_log] =
logs = [
%{
address_hash: "0x0BE9e53fd7EDaC9F859882AfdDa116645287C629",
block_number: 23_704_638,
block_hash: "0x79594150677f083756a37eee7b97ed99ab071f502104332cb3835bac345711ca",
data: "0x00000000000000000000000000000000000000000000002be19edfcf6b480000",
first_topic: "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
fourth_topic: nil,
index: 1,
second_topic: "0x0000000000000000000000000000000000000000000000000000000000000000",
third_topic: "0x000000000000000000000000fb76e9e7d88e308ab530330ed90e84a952570319",
transaction_hash: "0x4011d9a930a3da620321589a54dc0ca3b88216b4886c7a7c3aaad1fb17702d35"
},
%{
address_hash: "0x0BE9e53fd7EDaC9F859882AfdDa116645287C629",
block_number: 23_704_638,
block_hash: "0x79594150677f083756a37eee7b97ed99ab071f502104332cb3835bac345711ca",
data: "0x00000000000000000000000000000000000000000000002be19edfcf6b480000",
first_topic: "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c",
second_topic: "0x000000000000000000000000fb76e9e7d88e308ab530330ed90e84a952570319",
third_topic: nil,
fourth_topic: nil,
index: 2,
transaction_hash: "0x4011d9a930a3da620321589a54dc0ca3b88216b4886c7a7c3aaad1fb17702d35"
},
%{
address_hash: "0xf2eec76e45b328df99a34fa696320a262cb92154",
block_number: 3_530_917,
block_hash: "0x5a5e69984f78d65fc6d92e18058d21a9b114f1d56d06ca7aa017b3d87bf0491a",
data: "0x00000000000000000000000000000000000000000000000000e1315e1ebd28e8",
first_topic: "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
fourth_topic: nil,
index: 8,
second_topic: "0x000000000000000000000000e3f85aad0c8dd7337427b9df5d0fb741d65eeeb5",
third_topic: "0x0000000000000000000000000000000000000000000000000000000000000000",
transaction_hash: "0x185889bc91372106ecf114a4e23f4ee615e131ae3e698078bd5d2ed7e3f55a49"
},
%{
address_hash: "0xf2eec76e45b328df99a34fa696320a262cb92154",
block_number: 3_530_917,
block_hash: "0x5a5e69984f78d65fc6d92e18058d21a9b114f1d56d06ca7aa017b3d87bf0491a",
data: "0x00000000000000000000000000000000000000000000000000e1315e1ebd28e8",
first_topic: "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65",
second_topic: "0x000000000000000000000000e3f85aad0c8dd7337427b9df5d0fb741d65eeeb5",
third_topic: nil,
fourth_topic: nil,
index: 1,
transaction_hash: "0x185889bc91372106ecf114a4e23f4ee615e131ae3e698078bd5d2ed7e3f55a49"
}
]
expected = %{
tokens: [
%{
contract_address_hash: log_2.address_hash,
type: "ERC-20"
},
%{
contract_address_hash: log_1.address_hash,
type: "ERC-20"
}
],
token_transfers: [
%{
token_ids: nil,
amount: Decimal.new(63_386_150_072_297_704),
block_number: log_2.block_number,
log_index: log_2.index,
from_address_hash: truncated_hash(log_2.second_topic),
to_address_hash: truncated_hash(log_2.third_topic),
token_contract_address_hash: log_2.address_hash,
transaction_hash: log_2.transaction_hash,
token_type: "ERC-20",
block_hash: log_2.block_hash
},
%{
block_number: log_1.block_number,
log_index: log_1.index,
from_address_hash: truncated_hash(log_1.second_topic),
to_address_hash: truncated_hash(log_1.third_topic),
token_contract_address_hash: log_1.address_hash,
token_ids: nil,
transaction_hash: log_1.transaction_hash,
token_type: "ERC-20",
block_hash: log_1.block_hash,
amount: Decimal.new(809_467_672_956_315_893_760)
}
]
}
env = Application.get_env(:explorer, Explorer.Chain.TokenTransfer)
Application.put_env(
:explorer,
Explorer.Chain.TokenTransfer,
Keyword.put(env, :whitelisted_weth_contracts, [
log_1.address_hash |> to_string() |> String.downcase(),
log_2.address_hash |> to_string() |> String.downcase()
])
)
assert TokenTransfers.parse(logs) == expected
Application.put_env(:explorer, Explorer.Chain.TokenTransfer, env)
end
end
defp truncated_hash("0x000000000000000000000000" <> rest) do

@ -569,6 +569,10 @@ config :explorer, Explorer.Migrator.SanitizeIncorrectNFTTokenTransfers,
batch_size: ConfigHelper.parse_integer_env_var("SANITIZE_INCORRECT_NFT_BATCH_SIZE", 100),
concurrency: ConfigHelper.parse_integer_env_var("SANITIZE_INCORRECT_NFT_CONCURRENCY", 1)
config :explorer, Explorer.Migrator.SanitizeIncorrectWETHTokenTransfers,
batch_size: ConfigHelper.parse_integer_env_var("SANITIZE_INCORRECT_WETH_BATCH_SIZE", 100),
concurrency: ConfigHelper.parse_integer_env_var("SANITIZE_INCORRECT_WETH_CONCURRENCY", 1)
config :explorer, Explorer.Chain.BridgedToken,
eth_omni_bridge_mediator: System.get_env("BRIDGED_TOKENS_ETH_OMNI_BRIDGE_MEDIATOR"),
bsc_omni_bridge_mediator: System.get_env("BRIDGED_TOKENS_BSC_OMNI_BRIDGE_MEDIATOR"),
@ -579,6 +583,9 @@ config :explorer, Explorer.Chain.BridgedToken,
config :explorer, Explorer.Utility.MissingBalanceOfToken,
window_size: ConfigHelper.parse_integer_env_var("MISSING_BALANCE_OF_TOKENS_WINDOW_SIZE", 100)
config :explorer, Explorer.Chain.TokenTransfer,
whitelisted_weth_contracts: ConfigHelper.parse_list_env_var("WHITELISTED_WETH_CONTRACTS", "")
###############
### Indexer ###
###############

Loading…
Cancel
Save