Лучшие практики Solidity для безопасности смарт-контрактов

блог 1НовостиДля разработчиковПредприятиеБлокчейн РазъяснениеМероприятия и конференцииПрессаИнформационные бюллетени

Contents

Подписывайтесь на нашу новостную рассылку.

Адрес электронной почты

Мы уважаем вашу конфиденциальность

ГлавнаяБлогРазработка блокчейна

Лучшие практики Solidity для безопасности смарт-контрактов

Вот несколько профессиональных советов, которые помогут укрепить ваши смарт-контракты Ethereum – от мониторинга до отметок времени. by ConsenSys21 августа, 2020Опубликовано 21 августа, 2020

герой солидности передовой опыт

Автор ConsenSys Diligence, наша команда экспертов по безопасности блокчейнов.

Если вы серьезно относитесь к безопасности смарт-контрактов и понимаете особенности EVM, пора рассмотреть некоторые шаблоны безопасности, характерные для языка программирования Solidity. В этом обзоре мы сосредоточимся на рекомендациях по безопасной разработке для Solidity, которые также могут быть полезны при разработке смарт-контрактов на других языках.. 

Хорошо, давай приступим.

Правильно используйте assert (), require (), revert ()

Удобные функции утверждать и требовать может использоваться для проверки условий и выдачи исключения, если условие не выполняется..

В утверждать функция должна использоваться только для проверки внутренних ошибок и проверки инвариантов.

В требовать Функция должна использоваться для обеспечения допустимых условий, таких как входные данные или переменные состояния контракта, или для проверки возвращаемых значений из вызовов внешних контрактов.. 

Следование этой парадигме позволяет формальным инструментам анализа проверять, что недопустимый код операции никогда не может быть достигнут: это означает, что инварианты в коде не нарушаются и что код формально проверяется..

прагма solidity ^ 0.5.0; Контракт Sharer {функция sendHalf (адрес оплачиваемого адреса) публичный оплачиваемый возврат (баланс uint) {require (msg.value% 2 == 0, "Требуется четное значение."); // Require () может иметь необязательную строку сообщения uint balanceBeforeTransfer = address (this) .balance; (bool success,) = addr.call.value (msg.value / 2) (""); требовать (успех); // Поскольку мы вернулись, если перевод не удался, // у нас не должно быть никакой возможности сохранить половину денег. assert (адрес (this) .balance == balanceBeforeTransfer – msg.value / 2); // используется для внутренней проверки ошибок адрес возврата (this) .balance; }} Язык кода: JavaScript (javascript)


Видеть SWC-110 & SWC-123

Используйте модификаторы только для проверок

Код внутри модификатора обычно выполняется перед телом функции, поэтому любые изменения состояния или внешние вызовы будут нарушать Проверки-Эффекты-Взаимодействие шаблон. Более того, эти утверждения также могут остаться незамеченными разработчиком, поскольку код модификатора может быть далек от объявления функции. Например, внешний вызов модификатора in может привести к атаке повторного входа:

Реестр контрактов {владелец адреса; функция isVoter (address _addr) external returns (bool) {// Code}} contract Election {Registry registry; модификатор isElposed (адрес _addr) {требуется (registry.isVoter (_addr)); _; } function vote () isElhibited (msg.sender) public {// Code}} Язык кода: JavaScript (javascript)

В этом случае контракт реестра может совершить повторную атаку, вызвав Election.vote () внутри isVoter ()..

Примечание: Использовать модификаторы для замены повторяющихся проверок условий в нескольких функциях, таких как isOwner (), в противном случае используйте require или revert внутри функции. Это делает код смарт-контракта более читаемым и легким для аудита..

Остерегайтесь округления с целочисленным делением

Все целочисленные деления округляются до ближайшего целого числа. Если вам нужна более высокая точность, подумайте об использовании множителя или сохраните числитель и знаменатель..

(В будущем Solidity получит фиксированная точка введите, что упростит задачу.)

// плохой uint x = 5/2; // Результат 2, все целые числа округляются до ближайшего целого числа Язык кода: JavaScript (javascript)

Использование множителя предотвращает округление в меньшую сторону, этот множитель необходимо учитывать при работе с x в будущем:

// хороший множитель uint = 10; uint x = (5 * множитель) / 2; Язык кода: JavaScript (javascript)

Сохранение числителя и знаменателя означает, что вы можете рассчитать результат числителя / знаменателя вне сети:

// хороший числитель uint = 5; знаменатель uint = 2; Язык кода: JavaScript (javascript)

Помните о компромиссах между абстрактные контракты и интерфейсы

И интерфейсы, и абстрактные контракты предоставляют настраиваемый и многократно используемый подход для смарт-контрактов. Интерфейсы, представленные в Solidity 0.4.11, похожи на абстрактные контракты, но не могут иметь реализованных функций. Интерфейсы также имеют ограничения, такие как невозможность доступа к хранилищу или наследование от других интерфейсов, что обычно делает абстрактные контракты более практичными. Хотя интерфейсы, безусловно, полезны для разработки контрактов до реализации. Кроме того, важно помнить, что если контракт наследуется от абстрактного контракта, он должен реализовывать все нереализованные функции посредством переопределения, иначе он также будет абстрактным..

Резервные функции

Делайте резервные функции простыми

Резервные функции вызываются, когда контракту отправляется сообщение без аргументов (или когда функция не соответствует), и имеет доступ только к 2300 газам при вызове из .send () или .transfer (). Если вы хотите иметь возможность получать эфир из .send () или .transfer (), самое большее, что вы можете сделать в резервной функции, – это зарегистрировать событие. Используйте подходящую функцию, если требуется расчет большего количества газа..

// плохая функция () к оплате {балансы [msg.sender] + = msg.value; } // хорошая функция deposit () payable external {balances [msg.sender] + = msg.value; } function () к оплате {требуется (msg.data.length == 0); испустить LogDepositReceived (msg.sender); } Язык кода: JavaScript (javascript)

Проверьте длину данных в резервных функциях

Поскольку резервные функции вызывается не только для передачи простого эфира (без данных), но и при отсутствии других функций, вы должны проверить, что данные пусты, если функция отката предназначена для использования только с целью регистрации полученного эфира. В противном случае вызывающие абоненты не заметят, если ваш контракт используется неправильно и вызываются несуществующие функции..

// плохая функция () payable {emit LogDepositReceived (msg.sender); } // хорошая функция () payable {require (msg.data.length == 0); испустить LogDepositReceived (msg.sender); } Язык кода: JavaScript (javascript)

Явно помечайте оплачиваемые функции и переменные состояния

Начиная с Solidity 0.4.0, каждая функция, получающая эфир, должна использовать модификатор payable, в противном случае, если транзакция имеет значение msg.value > 0 вернется (кроме случаев принуждения).

Примечание: То, что может быть неочевидным: модификатор payable применяется только к вызовам из внешних контрактов. Если я вызываю функцию non-payable в функции payable в том же контракте, функция non-payable не завершится с ошибкой, хотя msg.value все еще установлен.

Явно отметьте видимость в функциях и переменных состояния

Явно обозначьте видимость функций и переменных состояния. Функции могут быть внешними, общедоступными, внутренними или частными. Пожалуйста, поймите различия между ними, например, внешнего может быть достаточно вместо общедоступного. Для переменных состояния внешнее невозможно. Явная маркировка видимости упростит выявление неверных предположений о том, кто может вызвать функцию или получить доступ к переменной..

  • Внешние функции являются частью интерфейса контракта. Внешняя функция f не может быть вызвана изнутри (т.е. f () не работает, но this.f () работает). Внешние функции иногда более эффективны, когда они получают большие массивы данных..
  • Публичные функции являются частью интерфейса контракта и могут вызываться либо изнутри, либо через сообщения. Для переменных общедоступного состояния создается функция автоматического получения (см. Ниже)..
  • Доступ к внутренним функциям и переменным состояния возможен только изнутри, без использования этого.
  • Частные функции и переменные состояния видны только для контракта, в котором они определены, а не в производных контрактах.. Примечание: Все, что находится внутри контракта, видно всем сторонним наблюдателям блокчейна, даже частные переменные..

// плохой uint x; // значение по умолчанию является внутренним для переменных состояния, но его следует сделать явным function buy () {// по умолчанию используется открытый // открытый код} // хорошо uint private y; function buy () external {// вызывается только извне или с помощью this.buy ()} function utility () public {// вызывается как извне, так и изнутри: изменение этого кода требует обдумывания обоих случаев. } function internalAction () internal {// внутренний код} Язык кода: PHP (php)

Видеть SWC-100 и SWC-108

Заблокировать прагмы для конкретной версии компилятора

Контракты следует развертывать с той же версией компилятора и флагами, с которыми они тестировались чаще всего. Блокировка прагмы помогает гарантировать, что контракты не будут случайно развернуты с использованием, например, последней версии компилятора, которая может иметь более высокий риск необнаруженных ошибок. Контракты также могут быть развернуты другими, и прагма указывает версию компилятора, предназначенную исходными авторами..

// плохая надежность прагмы ^ 0.4.4; // хорошая солидность прагмы 0.4.4; Язык кода: JavaScript (javascript)

Примечание: версия с плавающей директорией (например, ^ 0.4.25) будет нормально компилироваться с 0.4.26-nightly.2018.9.25, однако ночные сборки никогда не должны использоваться для компиляции кода для производства..

Предупреждение: Операторам Pragma можно разрешить перемещаться, если контракт предназначен для использования другими разработчиками, как в случае с контрактами в библиотеке или пакете EthPM. В противном случае разработчику потребуется вручную обновить прагму для локальной компиляции..

Видеть SWC-103

Используйте события для отслеживания деятельности по контракту

Может быть полезно иметь возможность отслеживать деятельность контракта после его развертывания. Один из способов добиться этого – просмотреть все транзакции контракта, однако этого может быть недостаточно, поскольку вызовы сообщений между контрактами не записываются в цепочку блоков. Более того, он показывает только входные параметры, а не фактические изменения, вносимые в состояние. Также события могут использоваться для запуска функций в пользовательском интерфейсе..

контракт Благотворительность {отображение (адрес => uint) остатки; функция donate () оплачиваемая общедоступная {балансы [msg.sender] + = msg.value; }} контракт Game {function buyCoins () payable public {// 5% идет на благотворительность charity.donate.value (msg.value / 20) (); }} Язык кода: JavaScript (javascript)

Здесь игровой контракт сделает внутренний вызов Charity.donate (). Эта транзакция не будет отображаться в списке внешних транзакций благотворительной организации, а будет отображаться только во внутренних транзакциях..

Событие – это удобный способ зафиксировать то, что произошло в контракте. Созданные события остаются в блокчейне вместе с другими данными контракта и доступны для будущего аудита. Вот усовершенствование приведенного выше примера: события используются для получения истории пожертвований благотворительной организации..

контракт Charity {// определяем событие event LogDonate (uint _amount); отображение (адрес => uint) остатки; функция donate () оплачиваемая общедоступная {балансы [msg.sender] + = msg.value; // генерировать событие emit LogDonate (msg.value); }} контракт Game {function buyCoins () payable public {// 5% идет на благотворительность charity.donate.value (msg.value / 20) (); }} Язык кода: JavaScript (javascript)

Здесь все транзакции, которые проходят через контракт на благотворительность, прямо или нет, будут отображаться в списке событий этого контракта вместе с суммой пожертвованных денег..

Примечание: отдавайте предпочтение более новым конструкциям Solidity. Предпочитайте конструкции / псевдонимы, такие как самоуничтожение (а не самоубийство) и keccak256 (вместо sha3). Такие шаблоны, как require (msg.sender.send (1 ether)), также можно упростить до использования transfer (), как в msg.sender.transfer (1 ether). Проверить Журнал изменений Solidity для более похожих изменений.

Имейте в виду, что “встроенные модули” могут быть затенены.

В настоящее время возможно тень встроенные глобалы в Solidity. Это позволяет контрактам переопределять функциональность встроенных модулей, таких как msg и revert (). Хотя это предназначен, это может ввести пользователей контракта в заблуждение относительно истинного поведения контракта.

контракт PrendingToRevert {функция revert () внутренняя константа {}} контракт ExampleContract – PrendingToRevert {function somethingBad () public {revert (); }}

Пользователи контракта (и аудиторы) должны знать полный исходный код смарт-контракта любого приложения, которое они намереваются использовать..

Избегайте использования tx.origin

Никогда не используйте tx.origin для авторизации, другой контракт может иметь метод, который будет вызывать ваш контракт (например, где у пользователя есть средства), и ваш контракт авторизует эту транзакцию, поскольку ваш адрес находится в tx.origin.

контракт MyContract {владелец адреса; функция MyContract () public {owner = msg.sender; } функция sendTo (адрес получателя, количество uint) public {require (tx.origin == owner); (bool success,) = Receiver.call.value (количество) (""); требовать (успех); }} контракт AttackingContract {MyContract myContract; адрес злоумышленника; функция AttackingContract (адрес myContractAddress) общедоступная {myContract = MyContract (myContractAddress); атакующий = msg.sender; } function () public {myContract.sendTo (атакующий, msg.sender.balance); }} Язык кода: JavaScript (javascript)

Вы должны использовать msg.sender для авторизации (если другой контракт вызывает ваш контракт, msg.sender будет адресом контракта, а не адресом пользователя, который вызвал контракт).

Вы можете прочитать больше об этом здесь: Документы по Solidity

Предупреждение: Помимо проблемы с авторизацией, есть вероятность, что tx.origin будет удален из протокола Ethereum в будущем, поэтому код, использующий tx.origin, не будет совместим с будущими выпусками. Виталик: «НЕ думайте, что tx.origin и дальше будет использоваться или иметь смысл».

Также стоит отметить, что, используя tx.origin, вы ограничиваете взаимодействие между контрактами, потому что контракт, использующий tx.origin, не может использоваться другим контрактом, поскольку контракт не может быть tx.origin.

Видеть SWC-115

Зависимость от отметки времени

При использовании метки времени для выполнения важной функции в контракте необходимо учитывать три основных момента, особенно когда действия связаны с переводом средств..

Манипуляция меткой времени

Имейте в виду, что меткой времени блока может управлять майнер. Учти это договор:

uint256 постоянная частная соль = block.timestamp; function random (uint Max) постоянные частные возвраты (результат uint256) {// получить лучшее начальное число для случайности uint256 x = salt * 100 / Max; uint256 y = соль * номер блока / (соль% 5); uint256 seed = block.number / 3 + (salt% 300) + Last_Payout + y; uint256 h = uint256 (block.blockhash (seed)); return uint256 ((h / x))% Max + 1; // случайное число от 1 до макс.} Язык кода: PHP (php)

Когда контракт использует временную метку для заполнения случайного числа, майнер может фактически опубликовать временную метку в течение 15 секунд после проверки блока, эффективно позволяя майнеру предварительно вычислить вариант, более благоприятный для его шансов в лотерее. Метки времени не случайны и не должны использоваться в этом контексте..

Правило 15 секунд

В Желтая бумага (Эталонная спецификация Ethereum) не устанавливает ограничения на то, сколько блоков может дрейфовать во времени, но это указывает что каждая временная метка должна быть больше, чем временная метка своего родителя. Популярные реализации протокола Ethereum Гет и Паритет оба отклоняют блоки с отметкой времени более 15 секунд в будущем. Таким образом, хорошее практическое правило при оценке использования временных меток: если масштаб вашего зависящего от времени события может изменяться на 15 секунд и сохранять целостность, безопасно использовать блок..

Избегайте использования block.number в качестве отметки времени

Можно оценить разницу во времени, используя свойство block.number и среднее время блока, однако это не является перспективным, поскольку время блока может измениться (например, реорганизация вилки и бомба сложности). При распродаже, охватывающей дни, правило 15 секунд позволяет получить более надежную оценку времени..

Видеть SWC-116

Предупреждение о множественном наследовании

При использовании множественного наследования в Solidity важно понимать, как компилятор составляет граф наследования..

контракт Final {uint public a; функция Final (uint f) public {a = f; }} контракт B является окончательным {int публичный сбор; функция B (uint f) Final (f) public {} function setFee () public {fee = 3; }} контракт C является окончательным {int публичный сбор; функция C (uint f) Final (f) общедоступная {} функция setFee () public {fee = 5; }} контракт A – это B, C {function A () public B (3) C (5) {setFee (); }} Язык кода: PHP (php)

Когда контракт развернут, компилятор будет линеаризовать наследование справа налево (после ключевого слова родители перечислены от наиболее базового до наиболее производного). Вот линеаризация контракта A:

Финал <- B <- C <- А

В результате линеаризации размер комиссии будет равен 5, поскольку контракт C является наиболее производным. Это может показаться очевидным, но представьте себе сценарии, в которых C может скрывать важные функции, переупорядочивать логические предложения и заставлять разработчика писать пригодные для использования контракты. Статический анализ в настоящее время не вызывает проблем с затененными функциями, поэтому его необходимо проверять вручную..

Чтобы внести свой вклад, на Github от Solidity есть проект со всеми проблемами, связанными с наследованием.

Видеть SWC-125

Используйте тип интерфейса вместо адреса для обеспечения безопасности типов

Когда функция принимает адрес контракта в качестве аргумента, лучше передать интерфейс или тип контракта, чем необработанный адрес. Если функция вызывается в другом месте исходного кода, компилятор предоставит дополнительные гарантии безопасности типов..

Здесь мы видим две альтернативы:

Валидатор контракта {функция проверки (uint) внешних возвратов (bool); } контракт TypeSafeAuction {// хорошая функция validateBet (Validator _validator, uint _value) внутренний возврат (bool) {bool valid = _validator.validate (_value); возврат действителен; }} контракт TypeUnsafeAuction {// плохая функция validateBet (адрес _addr, uint _value) внутренний возврат (bool) {Validator validator = Validator (_addr); bool valid = validator.validate (_value); возврат действителен; }} Язык кода: JavaScript (javascript)

Преимущества использования вышеуказанного контракта TypeSafeAuction можно увидеть в следующем примере. Если validateBet () вызывается с аргументом адреса или с типом контракта, отличным от Validator, компилятор выдаст эту ошибку:

контракт NonValidator {} Аукцион контракта – TypeSafeAuction {NonValidator nonValidator; функция bet (uint _value) {bool valid = validateBet (nonValidator, _value); // TypeError: недопустимый тип аргумента в вызове функции. // Недействительное неявное преобразование из контракта NonValidator // в контракт Validator запрошен. }} Язык кода: JavaScript (javascript)

Избегайте использования extcodesize для проверки внешних аккаунтов

Следующий модификатор (или аналогичная проверка) часто используется для проверки того, был ли звонок сделан из внешней учетной записи (EOA) или из контрактной учетной записи:

// неверный модификатор isNotContract (address _a) {uint size; сборка {size: = extcodesize (_a)} требует (size == 0); _; } Язык кода: JavaScript (javascript)

Идея проста: если адрес содержит код, это не EOA, а контрактный аккаунт. тем не мение, у контракта нет исходного кода, доступного во время строительства. Это означает, что пока конструктор работает, он может вызывать другие контракты, но extcodesize для его адреса возвращает ноль. Ниже приведен минимальный пример, показывающий, как эту проверку можно обойти:

контракт OnlyForEOA {uint публичный флаг; // неверный модификатор isNotContract (address _a) {uint len; сборка {len: = extcodesize (_a)} требует (len == 0); _; } функция setFlag (uint i) public isNotContract (msg.sender) {flag = i; }} контракт FakeEOA {конструктор (адрес _a) общедоступный {OnlyForEOA c = OnlyForEOA (_a); c.setFlag (1); }} Язык кода: JavaScript (javascript)

Поскольку адреса контрактов могут быть предварительно вычислены, эта проверка также может завершиться ошибкой, если она проверяет адрес, который пуст в блоке n, но для которого есть контракт, развернутый в некотором блоке больше n.

Предупреждение: В этом вопросе есть нюансы. Если ваша цель – предотвратить возможность вызова вашего контракта другими контрактами, вероятно, будет достаточно проверки extcodesize. Альтернативный подход – проверить значение (tx.origin == msg.sender), хотя это также имеет недостатки.

Могут быть и другие ситуации, в которых проверка extcodesize будет служить вашим целям. Описание их всех здесь выходит за рамки возможностей. Понять, что лежит в основе поведения EVM, и использовать свое суждение.

Безопасен ли ваш код блокчейна??

Закажите однодневную выборочную проверку у наших экспертов по безопасности. Забронируйте сегодня DiligenceБезопасностьУмные контрактыSolidityНовостная рассылкаПодпишитесь на нашу рассылку, чтобы получать последние новости Ethereum, корпоративные решения, ресурсы для разработчиков и многое другое.Адрес электронной почтыЭксклюзивный контентКак создать успешный блокчейн-продуктВебинар

Как создать успешный блокчейн-продукт

Как настроить и запустить узел EthereumВебинар

Как настроить и запустить узел Ethereum

Как создать собственный API EthereumВебинар

Как создать собственный API Ethereum

Как создать социальный токенВебинар

Как создать социальный токен

Использование инструментов безопасности при разработке смарт-контрактовВебинар

Использование инструментов безопасности при разработке смарт-контрактов

Будущее финансовых цифровых активов и DeFiВебинар

Будущее финансов: цифровые активы и DeFi

Mike Owergreen Administrator
Sorry! The Author has not filled his profile.
follow me
Like this post? Please share to your friends:
Adblock
detector
map