Reentrancy attack для чайникoff
Несколько слов о том, что это за атака, как оно работает, к чему может привести и пара примеров из жизни.
Концепт
Давайте представим, что мы наткнулись на этот прекрасный псевдокод.
- Если переменная timesCalled > 0, то функция завершается ошибкой
- Вызываем какую-то функцию из стороннего контракта
- увеличиваем переменную timesCalled на 1
По логике вещей, эта функция может быть вызвана один раз, после чего каунтер timesCalled увеличивается и становится равен 1. Все последующие попытки выполнить эту функцию будут заканчиваться ошибкой.
Так ли это? Зависит от того, что происходит в externalContractFunction.
Для наглядности того, что мы получим в итоге, в badFunction заменим вызов externalContractFunction на код самой badFunction
В итоге в начале мы имеем набор одинаковых проверок каунтера timesCalled и все они пройдут успешно, т.к. все увеличения каунтера происходят в самом конце. Как итог, в случае если externalContractFunction будет вызвана 2 раза - значение timesCalled после выполнения всего кода будет 2, если 6 раз - то 6 и т.п.
В примере выше мы сделали одно небольшое изменение - мы поставили вызов externalContractFunction в самый конец. Попробуем развернуть код и посмотреть, что у нас выходит теперь.
После перестановки мы имеем последовательные блоки проверки каунтера и его увеличения, как итог - вызов goodFunction завершится ошибкой в строке 4.
Ближе к реальности
Теперь разберем самую частую ошибку в контрактах, которая приводит к появлению уязвимости.
В функции mintPublic можно заметить знакомый паттерн.
Но в примере с badFunction мы добились эксплуатирования уязвимости за счет кастомного кода в вызываемой фунции, что же не так с _safeMint?
А не так с _safeMint то, что слово safe здесь обозначает только то, что её невозможно использовать для минта токена на контракт, с которого будет невозможно этот токен вывести.
Эта "безопасность" обеспечивается проверкой контракта на поддержку определенного интерфейса, а проверка, в свою очередь, пытается вызвать на контракте вызывающем _safeMint функцию onERC721Received.
Таким образом, атака на подобный контракт выглядит так.
- Деплоим контракт с кастомной функцией onERC721Received
- С нашего атакующего контракта начинаем минт уязвимого контракта
- Уязвимый контракт во время выполнения _safeMint вызывает на атакующем контракте onERC721Received, эта функция в свою очередь снова вызывает mintPublic на уязвимом контракте
Примеры из жизни
Reentrancy остается большой проблемой до сих пор для многих контрактов. Не так давно Azuki добавили дополнительный слой безопасности в их новейшую версию ERC721A, которая закрывает дыру в _safeMint, но опасность существует всегда, пока разработчики ставят вызов сторонних функций перед внесением изменений в контракт.
Ниже несколько примеров из реальных контрактов недавно созданных коллекций. Для удобства самостоятельного разбора:
- вызов внешней функции красным
- необходимые изменения в контракте зеленым
SPOODEEMOONWTF
При минте SPOODEEMOONWTF с контракта, можно не только обойти ограничение в количестве токенов на кошелек, но и глобальное ограничение количества токенов.
LACOSTE
Функция _mintNFT внутренняя, но вызывается при минте токенов. Аптейт количества сминченных кошельком токенов происходит в функции _updateClaimedNFT. При минте с атакующего контракта возможно обойти ограничение по количеству токенов на кошелек.
GossamerSeed
Здесь так же можно обойти ограничение на количество минт мест на кошелек.