В этой статье мы разберемся, что такое пулы ликвидности и как их реализуют в defi приложениях.
Для начала.
Пулы ликвидности – это смарт-контракты, содержащие заблокированные крипто-токены, которые были предоставлены пользователями платформы. Они самоисполняются и не нуждаются в посредниках, чтобы заставить их работать. Они поддерживаются другими частями кода, такими как автоматизированные маркет-мейкеры (AMM) , которые помогают поддерживать баланс в пулах ликвидности с помощью математических формул.
За предоставляемые криптоактивы провайдеры ликвидности получают долю от торговых комиссий. Или токены протокола.
Реализация пула ликвидности в беттинг-протоколе.
Давайте немного погрузимся в беттинг.
Есть conditions — элементарное событие, на которое можно сделать скидку (да/нет или другой простой вариант). Conditions генерят определенную комиссию, все это заливается в пул ликвидности. Прибыль распределяется между провайдерами ликвидности, в соответствии с их долей.
Но эта простая схема неприменима к событиям в реальном мире, которые происходят вне блокчейна. Рандомный пользователь может попытаться обмануть систему: например, кто-то во время матча может увидеть, что команда выигрывает и по ходу дела внести большую ликвидность, и потом вывести почти всю прибыль, которую принесло событие. Таким образом кто-то может украсть ликвидность у других пользователей без риска.

Самое простое решение — просто считать, кто и в какой condition положит деньги. Но в этом случае приходится хранить данные про каждого холдера депозита: какую сумму он внес и когда, а потом каким-то образом это все синхронизировать. Явно, что это не будет работать на больших данных.
Что придумали.
Есть такая структура данных — дерево отрезков. Она позволяет достаточно быстро выполнять математические операции внутри массива. Операции могут быть разные: найти большее или меньшее значение, их произведения, или, как в нашем случае, найти сумму.

Дерево следует заполнять, начиная с листьев, корневые значения показывают сумму. Если нужно посчитать сумму, например, со второго элемента по шестой: мы можем смотреть по родителям листков и таким образом можно быстрее посчитать. С ростом элементов эффективность сразу заметна такой системы.
Работает эта структура данных за О-большое от логарифма.
Навигация по дереву.
Дерево удобнее нумеровать не с 0, а с 1. Тогда мы получим удобную систему: у нас всегда левый ребенок — родитель, умноженный на два. А правый — родитель, умноженный на два + ребенок. Четные индексы — слева, правые индексы — нечетные. Легко подниматься по дереву вверх.
В этом примере четыре листочка — четыре депозита ликвидности. При такой структуре, максимум депозитов в пул ликвидности — четыре. Azuro использует около триллиона листочков. И это не требует много GAS fee.

Как работают механизмы в дереве.
Первый провайдер заливает $100 — мы записываем его в четвертый листик, после чего он попадает во второй, а потом и в первый. Второй провайдер, который внес $200, проделывает такой же путь. После этого в первом листике становится $300.
nodeAddLiquidity(100$)
+--------------------------------------------+
|                    1 (100$)                |
+------------------------+-------------------+
|         2 (100$)       |         3         |
+-------------+----------+---------+---------+
|   4 (100$)  |     5    |    6    |    7    |
+-------------+----------+---------+---------+
     +100$
nodeAddLiquidity(200$)
+--------------------------------------------+
|                    1 (300$)                |
+------------------------+-------------------+
|         2 (300$)       |         3         |
+-------------+----------+---------+---------+
|   4 (100$)  | 5 (200$) |    6    |    7    |
+-------------+----------+---------+---------+
                  +200$На каждый condition мы отправляем некоторое подкрепление коэффициентов. В любом бизнесе есть потребность гарантировать выплаты. Поэтому мы можем брать ассеты из пула ликвидности. Но мы делаем такое «ленивое» обновление, не достаем средства из каждого листочка, а забираем из полных корневых элементов (второго и первого листочка). При выводе ликвидности важно запомнить последний полный элемент. Просто нужно хранить его и итерировать с каждым обновлением ликвидности.
remove(30$)
+--------------------------------------------+
|                    1 (270$)                |
+------------------------+-------------------+
|         2 (270$)       |         3         |
+-------------+----------+---------+---------+
|   4 (100$)  | 5 (200$) |    6    |    7    |
+-------------+----------+---------+---------+Чтобы избежать обмана, когда во время активного condition, кто-то может внести большой объем ликвидности — мы можем обновить только его родителя в дереве.
addLimit(15$, 5)
+15$  [4, 5]
+--------------------------------------------+
|                    1 (585$)                |
+------------------------+-------------------+
|         2 (285$)       |    3 (300$)       |
+-------------+----------+---------+---------+
|    4 (100$) | 5 (200$) | 6 (300$)|    7    |
+-------------+----------+---------+---------+В случае, когда протокол возвращает прибыль — он передает сумму, которую возвращает и последний полный элемент. Например, если это 5, мы можем понять, что это был правый (нечетный) ребенок, значит он — последний (так как у родителя всего два листочка). Тогда мы обновляем только верхнего родителя. При выводе листки тоже не обновляются, также вывод вычисляется через значения родителя. Выгрузка из газ-репортера, при триллионе элементов показывает всего 300 000 газа, что примерно, как swap.
nodeWithdraw(5) 
+--------------------------------------------+
|                     1 (300$)               |
+------------------------+-------------------+
|           2 (0$)       |    3 (300$)       |
+-------------+----------+---------+---------+
|    4 (0$)   |  5 (0$)  | 6 (300$)|    7    |
+-------------+----------+---------+---------+
                  -190$ 
          