Уязвимость находилась в контракте BitcoinReserveOffering (BRO). Чтобы понять механику атаки, важно разобраться в двух стандартных механизмах.
Минт — это создание новых токенов. В данном случае пользователь вызывает функцию mint(), чтобы получить BRO-токены в обмен на внесенные средства.
safeTransfer и callback. При переводе токена на адрес контракта стандарт требует, чтобы контракт-получатель подтвердил прием через специальную функцию — onERC721Received(). Это называется callback, то есть функция обратного вызова: протокол автоматически «уведомляет» получателя и ждет подтверждения.
Уязвимость возникала из некорректного взаимодействия этих двух механизмов. Во время исполнения mint() контракт выполнял безопасный перевод токена, который запускал callback onERC721Received(). Этот callback срабатывал до завершения основного учета в mint() — и самостоятельно выпускал токены. После того как callback завершался, основной поток mint() доходил до своего собственного выпуска токенов и выпускал их снова. Один и тот же депозит учитывался дважды в рамках одного сценария исполнения.
Это callback-driven double mint — баг двойного учета: не два независимых вызова и не взлом защиты, два участка кода внутри одного выполнения функции, каждый из которых добросовестно делает свою работу, не зная о том, что другой уже сделал то же самое.
Представьте: вы сдаете чемодан в багаж в аэропорту. Сотрудник на стойке регистрации выдает вам квитанцию — и в этот момент его коллега из подсобки, не зная об этом, выдает вторую квитанцию на тот же чемодан. Багаж один, а квитанций две. Обе настоящие. Обе действительные. Именно это произошло с контрактом BRO — один депозит порождал два подтверждения выпуска токенов.
Атакующий повторил этот цикл 22 раза внутри одной транзакции. Поскольку все итерации происходили в рамках одной транзакции, внутренний обменный курс между ними не обновлялся. EVM (виртуальная машина Ethereum, среда исполнения смарт-контрактов) фиксирует изменения состояния только после завершения транзакции целиком. Стартовый баланс в ~135 BRO был искусственно раздут до ~567 млн BRO.