ElasticSwap: ошибка в формуле приводит новых поставщиков ликвидности к потере средств
Доброй охоты! Когда изучаешь код DeFi проектов, неизбежно встречаешь математические формулы, разобраться в которых часто очень сложно. Хотим рассказать про такого рода уязвимость, обнаруженную при аудите проекта ElasticSwap на площадке Code4rena в январе 2022 года.
Обзор проекта ElasticSwap
ElasticSwap — это первый автоматизированный маркет-мейкер (AMM), специально созданный для поддержки ребейзинг-токенов. Особенностью таких токенов является механизм изменения их количества в обращении, т.е. эластичное предложение токенов на рынке. CoinMarketCap дает такое объяснение: "Ребейзинг-токены похожи на стейблкоины в том смысле, что и те и другие имеют целевую цену. Однако, в отличие от стейблкоинов, ребейзинг-токены имеют эластичное предложение, а значит циркулирующее предложение токенов на рынке корректируется в соответствии со спросом и предложением без изменения суммарной стоимости актива в кошельках пользователей."
Обычно, AMM-проекты отказываются от поддержки подобных токенов, т.к. приходится учитывать еще и изменение баланса в результате ребейзинга. Команда ElasticSwap решает эту проблему. Поставщики ликвидности теперь могут получать ожидаемые в результате ребейзинга токены, при этом обеспечивая ликвидность в пулах ElasticSwap.
Определения, которые могут потребоваться для понимания деталей уязвимости:
exchange
— один экземпляр AMM
, который представляет пару baseToken
и quoteToken
.
baseToken
— произвольный токен ERC20
, который может быть ребейзинг-токеном (с эластичным предложением). Например, sOHM
, sKLIMA
и т. д.
quoteToken
— произвольный токен ERC20
, который не является ребейзинг-токеном (имеет фиксированное предложение).
decay
— результат дисбаланса токенов, возникающий сразу после того, как в baseToken
происходит ребейз.
Детали уязвимости
Влияние
Выявленная проблема с формулой является причиной, по которой поставщики ликвидности, при депозите только в один из токенов пары, получают меньшее значение quoteToken
, чем предполагается протоколом.
Доказательство концепции
Рассмотрим случай, когда ребейз для baseToken
происходит вверх:
Согласно документации ElasticSwap и соответствующему коду, Gamma
- это отношение количества акций (ΔRo
), полученных при вызове addLiquidity()
, к новому значению переменной totalSupply
(общее количество выпущенных акций, равное Ro' = Ro + ΔRo
):
ΔRo = (Ro/(1 - Gamma)) * Gamma ⟺ ΔRo * ( 1 - Gamma ) = Gamma * Ro ΔRo - Gamma * ΔRo = Gamma * Ro ΔRo = Gamma * Ro + Gamma * ΔRo ΔRo Gamma = --------- Ro + ΔRo
Gamma = ΔY / Y' / 2 * ( ΔX / Alpha^ )
ΔY
- это количество quoteToken
, добавленное новым поставщиком ликвидности в пул (см. L#277);
Y'
- новое значение общего количества quoteToken
(Y
) после вызова addLiquidity()
, т.е. Y' = Y + ΔY
(см. L#272 и L#278);
ΔX
- это ΔY * Omega
(см. L#259-263 и L#279);
Alpha^
- это Alpha - X
(см. L#234-235 и L#280);
- Начальное состояние:
X = Alpha = 1
,Y = Beta = 1
,Omega = X/Y = 1
- Затем происходит ребейз
baseToken
вверх: пустьAlpha
становится 10 - Тогда текущее состояние будет:
Alpha = 10
,X = 1
,Y = Beta = 1
,
Затем новый поставщик ликвидности вызывает addLiquidity()
, допустим, с 4 quoteToken
:
4 4 * Omega 16 Gamma = ------------ * ------------ = ---- (1+4) * 2 10 - 1 90
baseToken
у нового поставщика:10 * 16 / 90 = 160 / 90 = 1.7777777777777777
quoteToken
у нового поставщика:(1+4) * 16 / 90 = 80 / 90 = 0.8888888888888888
- В выражении
quoteToken
, общее количество будет:160 / 90 / Omega + 80 / 90 = 240 / 90 = 2.6666666666666665
В результате новый поставщик ликвидности терпит убытки в размере 4 - 240 / 90 = 1.3333333333333333
, выраженные в quoteToken
.
Теперь рассмотрим случай, когда ребейз для baseToken
происходит вниз:
Согласно документации ElasticSwap и соответствующему коду, Gamma
так же, как и в предыдущем случае, является отношением количества акций (ΔRo
), полученных при вызове addLiquidity()
, к новому значению переменной totalSupply
(общее количество выпущенных акций, равное Ro' = Ro + ΔRo
).
Gamma = ΔX / X / 2 * ( ΔXByQuoteTokenAmount / Beta^ )
ΔX
это количество baseToken
, добавленное новым поставщиком ликвидности в пул (см. L#357);
X
- это balanceOf
baseToken
(см. L#358);
ΔXByQuoteTokenAmount
- это ΔX / Omega
, величина ΔX
, выраженная в quoteToken
(см. L#318-322, L#329-333 и L#359);
Beta^
- это maxΔX / Omega
, величина maxΔX
, выраженная в quoteToken
;
maxΔX = X - Alpha
(см. L#304-305, L#318-322, L#341-342 и L#360);
- Начальное состояние:
X = Alpha = 10
,Y = Beta = 10
,Omega = X/Y = 1
- Затем происходит ребейз
baseToken
вниз: пустьAlpha
становится 1 - Тогда текущее состояние будет:
Alpha = 1
,X = 10
,Y = Beta = 10
,
Затем новый поставщик ликвидности вызывает addLiquidity()
, допустим, с 4 baseToken
:
4 4 / Omega 8 Gamma = ---------- * ---------------- = ---- 10 * 2 (10 - 1) / Omega 90
baseToken
у нового поставщика:(1 + 4) * 8 / 90 = 40 / 90 = 0.4444444444444444
quoteToken
у нового поставщика:10 * 8 / 90 = 80 / 90 = 0.8888888888888888
- В выражении
quoteToken
, общее количество будет:40 / 90 + 80 / 90 * Omega = 120 / 90 = 1.3333333333333333 < 4
В результате новый поставщик ликвидности терпит убытки в размере 4 - 120 / 90 = 2.6666666666666665
, выраженные в quoteToken
.
Подведение итогов
Данная уязвимость имеет категорию High
. Как видно, была проделана большая работа по изучению документации и кодовой базы для выявления такой уязвимости. Это позволило уберечь пользователей от потери средств. Предложенные аудитором исправления в формулах не были приняты командой проекта, поэтому мы не стали их приводить. Ознакомиться с шагами, проделанными разработчиками ElasticSwap для исправления данной ошибки можно здесь.