Данным постом начинаю цикл статей на тему безопасности смарт-контрактов Ethereum. Считаю эту тему весьма актуальной, так-как количество разработчиков лавинообразно растет, а уберечь от «граблей» — некому. Пока — переводы…
Оригинал — Scanning Live Ethereum Contracts for the «Unchecked-Send...»
Программирование смарт-контрактов в Ethereum, как известно, подвержено ошибкам [1] . Недавно мы увидели, что несколько
высококлассных смарт-контрактов, таких как King of the Ether и The DAO-1.0, содержали уязвимости, вызванные ошибками программирования.
Начиная с марта 2015 года программисты смарт-контрактов были предупреждены о конкретных опасностях программирования, которые могут возникнуть, когда контракты отправляют сообщения друг другу [6].
В нескольких руководствах по программированию содержится рекомендация, как избежать распространенных ошибок (в официальных документах Ethereum [3] и в независимом руководстве от UMD [2] ). Хотя эти опасности достаточно понятны, чтобы избегать их, последствия такой ошибки являются ужасными: деньги могут быть заблокированы, потеряны или украдены.
Насколько распространены ошибки, возникающие в результате этих опасностей? Есть ли еще уязвимые, но живые контракты на block-chain Ethereum? В этой статье мы отвечаем на этот вопрос, анализируя контракты на живом block-chain Ethereum с помощью нового инструмента анализа, который мы разработали.
Для отправки контрактом эфира на другой адрес, самым простым способом является использование ключевого слова send. Это действует как метод, определенный для каждого объекта. Например, следующий фрагмент кода может быть найден в смарт-контракте, который реализует настольную игру.
/*** Listing 1 ***/
if (gameHasEnded && !( prizePaidOut ) ) {
winner.send(1000); // отправить выигрыш победителю
prizePaidOut = True;
}
Проблема здесь в том, что метод send может выполниться с ошибкой. Если он не сработает, то победитель не получит деньги, однако переменная prizePaidOut будет установлена в True.
Существуют два разных случая, когда функция winner.send() может выйти из строя. Мы разберем различие между ними позже. Первый случай заключается в том, что адрес winner — это контракт (а не учетная запись пользователя), а код этого контракта генерирует исключение (например, если он использует слишком много «газа»). Если это так, то, возможно, в этом случае это «ошибка победителя». Второй случай менее очевиден. Виртуальная машина Ethereum имеет ограниченный ресурс, называемый «callstack» (глубина стека вызовов), и этот ресурс может быть использован другим кодом контракта, который был выполнен ранее в транзакции. Если callstack уже израсходован к моменту выполнения команды send , выполнение команды потерпит неудачу, независимо от того, как определен winner. Приз победителя будет уничтожен не по его вине!
Два предложения. Первое — проверить возвращаемое значение 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 К счастью, в этом случае разработчик контракта смог использовать несвязанную функцию в контракте в качестве «ручного переопределения» для выпуска застрявших средств. Менее скрупулезный администратор мог бы использовать ту же функцию, чтобы украсть эфир!
К сожалению, не доступен сервер mySQL