Взлом GMX, что это было?TLDR;
9 июля 2025 года хакер обнаружил и использовал уязвимость в смарт-контракте GMX v1, что привело к краже около 42 миллионов долларов из пула ликвидности GLP на сети Arbitrum. Уязвимость типа reentrancy позволилa манипулировать шортами и завышать стоимость токена GLP. В данный момент хакер вернул 40.5 миллионов по соглашению с биржей, а GMX v1 остановлен.Материал для поста взят из разборов от
SlowMist,
SolidityScan а так же из
X GMXКлючевые моментыАтака стала возможной из-за наличия двух фундаментальных недостатков дизайна GMX v1.
1. Некорректная работа с
globalShortAveragePrice. Система обновляла значение глобальной цены коротких позиций только при открытии шорт, а при закрытии - нет.
2. Мгновенный рост
globalShortSizes. При открытии шорт глобальный размер коротких позиций возрастает немедленно, что оказывает влияние на расчет AUM (Assets Under Management) - средства под управлением и это позволяет манипулировать ценой токена GLP в GlpManager.sol (токен ликвидности пула GMX).
В атаке так же использовалась уязвимость в
Timelock.enableLeverage механизме, который вызывался во время работы Keeper (это бот, через который происходит работа с ордерами). Глобально можно сказать, что проблема заключается в десинхронизации внутренних подсчетов механизма расчета цен GMX.
Подготовка к атаке
С атакующего контракта были исполнены две транзакции: открытие длинной позиции и ордер на уменьшение позиции, который позже должен был исполнить keeper.
Когда keeper получил ордер на уменьшение позиции, он вызвал
PositionManager::
executeDecreaseOrder, который в свою очередь сделал внутренний вызов
Timelock.
enableLeverage, который в свою очередь поставил флаг
_isLeverageEnabled контракта
Vault в позицию true (да, атака не самая простая). Постановка этого флага - ключевой момент.
После этого метод
OrderBook::
executeDecreaseOrder приступает к уменьшению позиции. Позиция скорректирована, и collateral (токен залогa), в данном случае WETH, должен вернуться на атакующий контракт. Но WETH перед трансфером разворачивается в ETH и это вызывает fallback функцию атакующего контракта - здесь и начинается reentrancy
fallback функция отправляет 3001 USDC в
Vault::
increasePosition и открывает шорт с х30 плечом, и соответствующий ордер на закрытие летит в keeper
Как работает Vault::increasePosition В функции
increasePosition в первую очередь происходит вызов внутренней функции
_validate, чтобы проверить разрешено ли использовать плечи (тот самый
_isLeverageEnabled, который заранее переведен в true). Такая ситуация возможно только если операцию проводит keeper, то есть прямой вызов контракта приведет к ошибке.
Поэтому атакующий и создал заранее ордер на уменьшение позиции, который исполнит keeper, и так получил доступ к использованию плечей в своей fallback функции, с помощью которой в осуществил повторный вход (
reentrancy) и вызвал
Vault::increasePosition, напрямую создав короткую позицию.
Ко всему прочему,
Vault::increasePosition обновляет значение
globalShortAveragePrices, когда шорт позиция открывается, а вот
Vault:: decreasePosition не обновляет, когда шорт закрывает.
Это позволило использовать эти 3к USDC многократно, чтобы повлиять на
globalShortAveragePrices.
В итоге хакеру удалось уменьшить среднюю цену шорта примерно в 57 раз относительно настоящей цены по маркету WBTC. Все эти операции проводились пока keeper исполнял ордер по уменьшению позиции.
Что было дальшеПосле того как keeper получил финальный ордер на закрытие позиции, он вызвал
OrderBook::
executeDecreaseOrder. Затем возврат ETH на атакующий контракт снова запустил fallback функцию, но на этот раз она:
- Взяла flashloan на Uniswap в размере 7.538кк USDC
- Вызвала
RewardRouterV2::
mintAndStakeGlp, где сминтила и стейкнула 4.129кк GLP за 6кк USDC