1. Сканирование Live Ethereum контрактов на ошибку «Unchecked-Send»
Оригинал — Scanning Live Ethereum Contracts for the «Unchecked-Send...»
Авторы: Zikai Alex Wen и Andrew Miller
Программирование смарт-контрактов в Ethereum, как известно, подвержено ошибкам [1] . Недавно мы увидели, что несколько
высококлассных смарт-контрактов, таких как King of the Ether и The DAO-1.0, содержали уязвимости, вызванные ошибками программирования.
Начиная с марта 2015 года программисты смарт-контрактов были предупреждены о конкретных опасностях программирования, которые могут возникнуть, когда контракты отправляют сообщения друг другу [6].
В нескольких руководствах по программированию содержится рекомендация, как избежать распространенных ошибок (в официальных документах Ethereum [3] и в независимом руководстве от UMD [2] ). Хотя эти опасности достаточно понятны, чтобы избегать их, последствия такой ошибки являются ужасными: деньги могут быть заблокированы, потеряны или украдены.
Насколько распространены ошибки, возникающие в результате этих опасностей? Есть ли еще уязвимые, но живые контракты на block-chain Ethereum? В этой статье мы отвечаем на этот вопрос, анализируя контракты на живом block-chain Ethereum с помощью нового инструмента анализа, который мы разработали.
Что такое ошибка «unchecked-send»?
Для отправки контрактом эфира на другой адрес, самым простым способом является использование ключевого слова send. Это действует как метод, определенный для каждого объекта. Например, следующий фрагмент кода может быть найден в смарт-контракте, который реализует настольную игру.
/*** Listing 1 ***/
if (gameHasEnded && !( prizePaidOut ) ) {
winner.send(1000); // отправить выигрыш победителю
prizePaidOut = True;
}
Проблема здесь в том, что метод send может выполниться с ошибкой. Если он не сработает, то победитель не получит деньги, однако переменная prizePaidOut будет установлена в True.
Существуют два разных случая, когда функция winner.send() может выйти из строя. Мы разберем различие между ними позже. Первый случай заключается в том, что адрес winner — это контракт (а не учетная запись пользователя), а код этого контракта генерирует исключение (например, если он использует слишком много «газа»). Если это так, то, возможно, в этом случае это «ошибка победителя». Второй случай менее очевиден. Виртуальная машина Ethereum имеет ограниченный ресурс, называемый «callstack» (глубина стека вызовов), и этот ресурс может быть использован другим кодом контракта, который был выполнен ранее в транзакции. Если callstack уже израсходован к моменту выполнения команды send , выполнение команды потерпит неудачу, независимо от того, как определен winner. Приз победителя будет уничтожен не по его вине!
Как можно избежать этой ошибки?
В документации Ethereum содержится краткое предупреждение об этой потенциальной опасности [3] :"Есть некоторая опасность при использовании send — передача завершается с ошибкой, если глубина стека вызовов составляет 1024 (это всегда может быть вызвано вызывающим), и также терпит неудачу, если у получателя заканчивается «газ». Поэтому, чтобы обеспечить безопасную передачу эфира, всегда проверяйте возвращаемое значение send или даже лучше: используйте шаблон, в котором получатель изымает деньги."
Два предложения. Первое — проверить возвращаемое значение send, чтобы убедиться, успешно ли оно завершено. Если это не так, то генерируйте исключение, чтобы откатить состояние назад.
/*** Listing 2 ***/
if (gameHasEnded && !( prizePaidOut ) ) {
if (winner.send(1000))
prizePaidOut = True;
else throw;
}
Это адекватное исправление для текущего примера, но не всегда это правильное решение. Предположим, мы модифицируем наш пример, чтобы, когда игра закончилась, победитель и проигравший откатили свое состояние назад. Очевидным применением «официального» решения было бы следующее:
/*** Listing 3 ***/
if (gameHasEnded && !( prizePaidOut ) ) {
if (winner.send(1000) && loser.send(10))
prizePaidOut = True;
else throw;
}
Однако это ошибка, поскольку она вводит дополнительную уязвимость. В то время как этот код защищает winner от атаки callstack, он также делает winner и loser уязвимыми друг для друга. В этом случае мы хотим предотвратить атаку callstack, но продолжаем выполнение, если команда send по какой-либо причине не сработает.
Поэтому даже лучшая передовая практика (рекомендованная в нашем «Руководстве программиста для Ethereum и Serpent», хотя она одинаково применима к Solidity), заключается в проверке наличия ресурса callstack. Мы можем определить макрос callStackIsEmpty (), который вернет ошибку, если и только если callstack пустой.
/*** Listing 4 ***/
if (gameHasEnded && !( prizePaidOut ) ) {
if (callStackIsEmpty()) throw;
winner.send(1000)
loser.send(10)
prizePaidOut = True;
}
Еще лучше рекомендация из документации Ethereum — «Использовать шаблон, в котором получатель забирает деньги», является немного загадочной, но имеет объяснение. Предложение состоит в том, чтобы реорганизовать ваш код, чтобы эффект неудачи send был изолирован, и влиял только на одного получателя за раз. Ниже приведен пример этого подхода. Однако этот совет также является анти-шаблоном. Он принимает на себя ответственность за проверку callstack самим получателям, что делает вероятными попадание в одну и ту же ловушку.
/*** Listing 5 ***/
if (gameHasEnded && !( prizePaidOut ) ) {
accounts[winner] += 1000
accounts[loser] += 10
prizePaidOut = True;
}
...
function withdraw(amount) {
if (accounts[msg.sender] >= amount) {
msg.sender.send(amount);
accounts[msg.sender] -= amount;
}
}
Многие высокоразвитые интеллектуальные контракты уязвимы. Лотерея «Король Эфира Трона» — наиболее известный случай этой ошибки [4] . Эта ошибка не была замечена, пока сумму 200 эфиров (стоимостью более 2000 долларов США по сегодняшней цене) не смог получить законный победитель лотереи. Соответствующий код в King of the Ether похож на код в листинге 2 К счастью, в этом случае разработчик контракта смог использовать несвязанную функцию в контракте в качестве «ручного переопределения» для выпуска застрявших средств. Менее скрупулезный администратор мог бы использовать ту же функцию, чтобы украсть эфир!
Продолжение Сканирование Live Ethereum контрактов на ошибку «Unchecked-Send». Часть 2