← Блог AMLConsensusamlconsensus.comПроверить адрес в боте
Постмортем эксплойта · Ethereum Mainnet

Анатомия взлома rsETH

Как через публичную keeper-функцию кастомного Safe-модуля и вредоносный хук Uniswap V4 из кошелька увели 2 900 rsETH — около $7,73 млн — одной атомарной транзакцией.

Потери
~$7,73 млн≈ 2 900 rsETH
Дата
15.09.202604:38:47 UTC
Блок
25 980 525Ethereum
Класс уязвимости
Confused deputyнебезопасный делегат-модуль
Затронуто
1 кошелёк Safeядро Safe и rsETH — целы

В двух предложениях. У жертвы на Safe (смарт-контрактный мультисиг) был установлен кастомный модуль-автоматизация, который управлял LP-позицией в Uniswap V4 и держал rsETH застейканным в Aave. У этого модуля была публичная keeper-функция, куда любой мог передать свои параметры — включая адрес пула и хука.

Атакующий заранее создал свой пул V4 с вредоносным хуком, затем дёрнул публичный keeper и заставил модуль, действуя от имени кошелька, вынуть rsETH из Aave и прогнать его через пул атакующего. Хук перехватил средства, а весь набор действий был упакован в один MEV-бандл и исполнен в том же блоке.

Действующие лица

Кто есть кто в этой транзакции

Все адреса публичны в блокчейне. Ниже — реконструкция ролей по ончейн-данным транзакции 0x0e7680…a705.

РольАдресЧто делает в атаке
Жертва0x40E93a52…D9f7AbA8Safe-кошелёк. Держал ~2 900 rsETH в виде aEthrsETH (застейкано в Aave V3). На нём висел уязвимый LP-модуль.
Атакующий0x0dC2c5D6…5dfe9b66Деплойер. 15.09 развернул связку вредоносных контрактов. Профинансирован через миксер RAILGUN — чтобы разорвать связь с источником средств.
Фейк-токен0x10605eE4…A0705Cb1Пустой ERC-20 «PAT», созданный атакующим только чтобы образовать вторую сторону пула V4 (rsETH ↔ PAT).
Получатель0xC70f00CD…8b80ea0Кошелёк, на который в итоге село ~2 882 rsETH (≈$7,43 млн) — чистая добыча после издержек.
MEV-исполнитель0x80BF7Db6…DBBD4e4Бот «Yoink», через который прошёл вызов yoink(). Заплатил билдеру блока (Ultrasound Money) ~18,9 ETH за гарантированное включение и порядок.
ИнфраструктураUniswap V4 PoolManager · Aave V3 Pool · Permit2Легитимные протоколы, которые атака использовала как инструменты. Ни один из них не был «взломан».
Отправная точка

Как это должно было работать

Safe — это не обычный кошелёк на приватном ключе, а смарт-контракт. Чтобы автоматизировать рутину (ребалансы, сбор наград, ролловер позиций), к нему подключают модули. Модуль — это доверенный контракт, который может двигать средства кошелька без подписи владельцев. В этом вся его польза и вся его опасность.

Владелец приватные ключи Safe (мультисиг) смарт-контракт-кошелёк хранит средства LP-модуль V4 кипер-автоматизация права двигать средства Aave V3 aEthrsETH владеет доверяет стейкает
Штатная цепочка доверия. Модуль получает право двигать средства кошелька один раз, при установке. Дальше он должен трогать только заранее одобренные протоколы и пулы. Именно этой проверки в кастомном модуле и не хватало.
Механика атаки

Как это сделали, по шагам

Вся атака — это одна связная последовательность, уложенная в один блок. Разберём её на четыре стадии: подготовка, «обман доверенного зама», извлечение и вывод.

1

Подготовка: атакующий строит ловушку

Заранее, тем же днём, атакующий развернул три вещи: пустой токен-пустышку PAT, вредоносный хук и создал в Uniswap V4 пул, где одной стороной идёт rsETH, второй — PAT, а полем hooks в ключе пула указан его собственный хук.

Ключевой момент V4: пул однозначно определяется структурой PoolKey, в которую входит адрес хука. Кто задаёт PoolKey — тот выбирает, чей код будет исполняться во время операций с пулом.

Атакующий 0x0dC2…9b66 Фейк-токен PAT вторая сторона пула Вредоносный хук код исполнится внутри пула Немного rsETH затравка ликвидности Пул Uniswap V4 rsETH ↔ PAT hooks = вредоносный хук деплоит создаёт пул
Ловушка готова. Сам по себе этот пул безобиден и ничего не стоит. Оружием он становится только в связке с чужим модулем, который согласится в него зайти.
2

Обман «доверенного зама»: публичный keeper

Модуль жертвы имел публичную функцию-кипер (multicall), задуманную для ботов-автоматизаторов. Проблема: она принимала параметры от вызывающего — какой пул и какой хук использовать — и исполняла действие через DELEGATECALL в контексте самого Safe, без проверки, что цель разрешена.

Атакующий просто вызвал этот keeper и подсунул PoolKey со своим хуком. С этого момента полномочия кошелька оказались направлены на инфраструктуру атакующего. Это классический confused deputy — доверенного исполнителя заставляют применить его права против его же хозяина.

Атакующий никаких прав на Safe Публичный keeper() multicall, открыт для всех принимает PoolKey извне ① вызов КОНТЕКСТ SAFE · полномочия кошелька LP-модуль DELEGATECALL исполняет как Safe Aave V3 redeem aEthrsETH → 2 900 rsETH Пул + хук атакующего перехват средств ② делегирует
Сердце эксплойта. Красная пунктирная рамка — это граница полномочий кошелька. Атакующий сам в неё не входит: он лишь задаёт цель, а всю работу за него делает доверенный модуль, исполняясь через delegatecall как сам Safe. Проверки «а разрешён ли этот пул?» между шагами ② и ③ не было.
3

Извлечение: aEthrsETH → rsETH → пул атакующего

Дальше всё детерминировано. Модуль, действуя как кошелёк, погасил позицию в Aave: сжёг 2 900 aEthrsETH и получил 2 900 «живых» rsETH. Затем завёл их в пул V4 через unlock/swap. Пока пул исполнял операцию, он передал управление хуку атакующего в колбэках — и тот развёл расчёт (deltas) так, что rsETH вышел на сторону атакующего, а взамен кошелёк «получил» бесполезный PAT.

aEthrsETH
Долговой/депозитный токен Aave: расписка на застейканный rsETH. Гасится 1:1 в rsETH.
Хук V4
Контракт, которому пул отдаёт управление до/после свопа и изменения ликвидности. Может менять расчёт средств.
unlock / deltas
В V4 расчёт идёт через «долги и кредиты». Кто контролирует хук — влияет на то, кто кому в итоге должен.
4

Вывод: атомарный MEV-бандл и оплата билдеру

Всё это упаковано в один атомарный бандл и исполнено в блоке 25 980 525. Транзакция, которую вы видели, — это yoink() бота Yoink. Из 2 900 извлечённых rsETH около 2 882 rsETH (~$7,43 млн) село на кошелёк-получатель, а ~18,9 ETH ушло билдеру блока (Ultrasound Money) как плата за гарантированное включение и нужный порядок.

Aave V3 2 900 aEthrsETH Контекст Safe 2 900 rsETH Пул + хук MEV-бандл «Yoink» Получатель ≈ 2 882 rsETH · $7,43 млн Билдер блока ≈ 18,9 ETH комиссия redeem swap добыча оплата
Итоговый баланс. Разница между 2 900 и 2 882 rsETH — это издержки: проскальзывание, затравка ликвидности и плата билдеру. Атомарность бандла означала, что если бы любая часть сорвалась, всё откатилось бы — риск исполнения для атакующего был близок к нулю.
Реконструкция трейса

Как выглядит вызов изнутри

Ниже — упрощённая реконструкция дерева вызовов (не дословный трейс Etherscan, а его смысловая схема). Красным выделен опасный DELEGATECALL, из-за которого чужой код исполнился с полномочиями кошелька.

Схема, а не сырой лог. Настоящую транзакцию можно открыть по хешу 0x0e7680…a705 и посмотреть вкладки State и Internal Txns.

Корень проблемы

Почему это вообще сработало

Ни Safe, ни Aave, ни Uniswap, ни сам протокол rsETH взломаны не были. Взломан был кастомный модуль — самая рискованная поверхность любого Safe, потому что модуль по определению обходит подписи владельцев.

Любой вызывающий задаёт цель Модуль = доверенный зам есть полномочия кошелька нет проверки: цель разрешена? Средства кошелька уходят по цели атакующего «сходи вот сюда» применяет права
Confused deputy («сбитый с толку зам»). Уязвимость не в том, что у модуля были права, а в том, что он позволял постороннему выбирать, куда эти права направить. Три ошибки сложились: публичный вход + параметры от вызывающего + delegatecall без allowlist целей.

Три ошибки, которые должны были совпасть

1. Публичность. Keeper-функцию оставили открытой для всех (это нормально для киперов), но не ограничили, что она может делать.
2. Параметры извне. Адрес пула и хука брались из аргументов вызова, а не из зашитого белого списка.
3. Delegatecall. Действие исполнялось в контексте Safe, то есть чужой хук получал полный авторитет кошелька, а не свои собственные права.

Защита

Как такое не допустить

Для тех, кто держит средства на Safe с кастомными модулями, или пишет такие модули. Каждый пункт закрывает одну из трёх ошибок выше.

Белый список пулов и хуков

Keeper должен ходить только по заранее зашитым PoolKey и адресам хуков. Параметр от вызывающего — только выбор из allowlist, а не произвольный адрес.

Никаких delegatecall на внешние цели

Если действие можно сделать обычным CALL с фиксированным получателем — так и делать. Delegatecall на код, который выбирает вызывающий, — почти всегда дыра.

Лимиты и таймлоки

Потолок на вывод за транзакцию, суточные лимиты, задержка на новые цели. Атака на $7,73 млн одним блоком не должна проходить без «стоп-крана».

Наименьшие привилегии для модуля

Модуль должен иметь право трогать только конкретные токены и площадки, а не весь баланс кошелька. Aave-позиция не обязана быть доступна LP-модулю.

Отдельный аудит кастомных модулей

Ядро Safe проверено годами. Ваш самописный модуль — нет. Именно он расширяет поверхность атаки и требует ревью в первую очередь.

Мониторинг и авто-стоп

Системы вроде Blockaid ловят аномальные вызовы к модулям. В связке с circuit breaker это шанс остановить транзакцию до, а не констатировать после.

Прикладной вывод для P2P и приёма средств. Средства с этого взлома уже расходятся по цепочке переводов — через часы они могут оказаться на адресе, который пришлёт вам «обычный» платёж. Биржа увидит эту связь при первом же выводе. Проверяйте контрагента до приёма: @amlconsensus_bot — санкции, блэклисты эмитентов, метки и экспозиция по консенсусу источников, бесплатно.