Written by Alex de Sousa

ICON 2.0: Realtime Decentralized Quote Prices

Cover by Maxim Hopman

ICON 2.0 is a smart contract platform with a unique interoperability proposition: Blockchain Transmission Protocol (BTP). BTP will enable decentralized and trustless interoperability between ICON 2.0 and all blockchain that implement the protocol.

At the time of publishing this article, the full BTP is not yet released. However, some of its features are already available to everyone. One of these features is a websocket to receive realtime updates on either (or both) blocks produced and event logs emitted.

This article will focus on how we can leverage this websocket connection to retrieve quote prices from Balanced Decentralized Exchange by using the Elixir ICON 2.0 SDK I wrote in the past few months.

Without further ado, let's go!

Let's go!

Prerequisites

This article assumes you have downloaded icon either within a new project using mix new or using a script e.g:

#/usr/bin/env elixir

Mix.install([
  {:icon, "~> 0.1"}
])

# ... rest of the script ...

You can also find the full script for getting sICX/bnUSD quotes here.

Quote Prices

When we want to exchange one token for another, we're presented with token pairs. These pairs have a base token, a quote token and, a quote price:

ICX/USD means ICX priced in USD

ICX/USD = $0.60: ICX (ICON's token) priced in USD is worth $0.60

If we, somehow, capture the event logs emitted every time someone exchanges one token for another, then we'll have realtime quote prices for any pair we want. In this article, we'll subscribe to Balanced sICX/bnUSD prices.

Note: sICX is staked ICX token and bnUSD is an algorithmic stablecoin pegged to the dollar called Balanced Dollars.

Getting the Swap Log

Balanced is built from several SCOREs (Smart Contract On Reliable Environment) and one of these SCOREs handles all operations related to the decentralized exchange (DEx). The main DEx operation is swapping tokens e.g. we can use the contract to exchange our sICX with bnUSD.

Furthermore, every time someone swaps a token, the SCORE will emit the following event:

Swap(int,Address,Address,Address,Address,Address,int,int,int,int,int,int,int,int,int)

We can query the SCORE's API with Icon.get_score_api/2 and get the meaning of each input. For that, we'll need the SCORE address, which is cxa0af3165c08318e988cb30993b3048335b94af6c in the Mainnet:

dex_score = "cxa0af3165c08318e988cb30993b3048335b94af6c"
identity = Icon.RPC.Identity.new(network_id: :mainnet)

{:ok, api} = Icon.get_score_api(identity, dex_score)

swap_definition =
  Enum.find(api, fn %{"name" => name, "type" => type} ->
    name == "Swap" and type == "eventlog"
  end)

In the variable swap_definition, we'll have the Swap event log definition:

%{
  "inputs" => [
    %{"indexed" => "0x1", "name" => "_id", "type" => "int"},
    %{"indexed" => "0x1", "name" => "_baseToken", "type" => "Address"},
    %{"name" => "_fromToken", "type" => "Address"},
    %{"name" => "_toToken", "type" => "Address"},
    %{"name" => "_sender", "type" => "Address"},
    %{"name" => "_receiver", "type" => "Address"},
    %{"name" => "_fromValue", "type" => "int"},
    %{"name" => "_toValue", "type" => "int"},
    %{"name" => "_timestamp", "type" => "int"},
    %{"name" => "_lpFees", "type" => "int"},
    %{"name" => "_balnFees", "type" => "int"},
    %{"name" => "_poolBase", "type" => "int"},
    %{"name" => "_poolQuote", "type" => "int"},
    %{"name" => "_endingPrice", "type" => "int"},
    %{"name" => "_effectiveFillPrice", "type" => "int"}
  ],
  "name" => "Swap",
  "type" => "eventlog"
}

Analyzing the Swap Event

With the previous definition, we can start making some assumptions about each event input and what things we need to get from them in order to have the price for sICX/bnUSD every time the event is emitted:

  • _id is the token pair identifier (indexed 1st position).
  • _baseToken is the SCORE address of the base token of the pair (indexed 2nd position).
  • _fromToken is the SCORE address of the token being sold (data 1st position).
  • _toToken is the SCORE address of the token being bought (data 2nd position).
  • _timestamp is the UNIX epoch timestamp in UTC in microseconds (data 7th position).
  • _endingPrice is the quote price after the swap in loop (data 12th position).

Either partially or fully, we can use the previous inputs to find the quote price we're looking for.

Detective

Protip: We can query the pool stats with the DEx function getPoolStats and search by ID. This will give us sICX/bnUSD pair has the ID 2. You can check it out yourself by running the following:

Icon.call(identity, dex_score, "getPoolStats", %{_id: 2},
  call_schema: %{_id: :integer}
)

At the time of writing this article, there are 39 pools, but not all of them have a name, so in this case using _baseToken, _fromToken, and _toToken addresses can help us figure out the actual pair name.

Realtime Updates

Elixir ICON 2.0 SDK has an Yggdrasil adapter for ICON's websocket. Thus we can use an Yggdrasil process to subscribe to the Swap event. In this case, the most important part is to define out channel correctly:

defmodule Quotes do
  use Yggdrasil
  alias Icon.Schema.Types.EventLog

  @dex_contract "cxa0af3165c08318e988cb30993b3048335b94af6c"
  @sicx_bnusd_id 2
  @signature "Swap(int,Address,Address,Address,Address,Address,int,int,int,int,int,int,int,int,int)"

  @channel [
    adapter: :icon,
    name: %{
      source: :event,
      data: %{
        addr: @dex_contract,
        event: @signature,
        indexed: [@sicx_bnusd_id, nil]
      },
      from_height: 47_077_000
    }
  ]

  def start_link, do: Yggdrasil.start_link(__MODULE__, [@channel])

  # ... handle_event/3 definition ...
end

Protip: I added from_height: 47_077_000 field to the channel's name for subscribing to an older block height. This way, we don't need to wait for a Swap event to happen and we can see some results right away. In general, this is only needed for testing purposes, but it wouldn't be needed if we just want the latest price.

Showing the Prices

The events we'll receive will have:

  • The _timestamp in microseconds in the 7th position of data.
  • The _endingPrice in the 12th position of data (_endingPrice * 10¹⁸).

So we can now define our handle_event/3 callback and finally print our quote price in the console:

defmodule Quotes do
  use Yggdrasil
  alias Icon.Schema.Types.EventLog

  # ... channel definition ...

  @impl Yggdrasil
  def handle_event(_channel, %EventLog{} = swap_event, _state) do
    [_, _, _, _, _, _, timestamp, _, _, _, _, price, _] = swap_event.data

    datetime =
      timestamp
      |> DateTime.from_unix!(:microsecond)
      |> DateTime.to_iso8601()

    price = price / 1_000_000_000_000_000_000

    IO.puts("[#{datetime}] sICX/bnUSD price: #{price}")

    {:ok, nil}
  end
end

If we execute this process:

Quotes.start_link()

then we'll get the following output:

[2022-03-07T16:17:15.095799Z] sICX/bnUSD price: 0.6762914917929946
[2022-03-07T16:33:31.390351Z] sICX/bnUSD price: 0.6765223727152583
[2022-03-07T16:19:25.140181Z] sICX/bnUSD price: 0.6763077935623969
[2022-03-07T16:06:10.885982Z] sICX/bnUSD price: 0.6696113966372721
[2022-03-07T16:28:17.210851Z] sICX/bnUSD price: 0.6765180152082558
[2022-03-07T16:27:41.288288Z] sICX/bnUSD price: 0.6764913565725723
... continues ...

Note: You can check out the full script here.

Beautiful

Conclusion

Though BTP is not fully released, we can already start leveraging some of its core features for our own advantage. Given ICON's block time is 3 seconds, this websocket connection gives us a tremendous advantage for building realtime bots for different purposes: from maximizing our yield to securing our favorite NFT latest drop.

ICON 2.0 definitely has a great advantage over many other slow blockchains and I believe its DeFi ecosystem will have a bright future!

The future is now

Avatar of Alex de Sousa

Alex de Sousa

Elixir alchemist. Tech enthusiast.