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

Откройте для себя Open Zeppelin, проверенную в бою фреймворк многоразовых смарт-контрактов Ethereum с открытым исходным кодом. Они предоставляют полезные контракты, в которых используются стандартные шаблоны безопасности, которые вы можете использовать в своих собственных контрактах. Их репозиторий solidity также является отличным ресурсом для изучения этих шаблонов. Один из таких шаблонов использует синглтон для предотвращения повторного входа уязвимостей.

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

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

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

pragma solidity ^0.4.24;

contract ReentrancyGuard {
  bool private reentrancyLock = false;
  modifier nonReentrant() {
    require(!reentrancyLock);
    reentrancyLock = true;
    _;
    reentrancyLock = false;
  }
}

Чтобы использовать эту функцию, контракт наследуется от ReentrancyGuard и получит логическую переменную состояния reentrancyLock и предварительное условие модификатор nonReentrant.

ReentrancyMock

Open Zeppelin предоставляет контракт ReentrancyMock, который имитирует три потенциальных типа атак, помеченных комментариями ниже:

  1. Когда функция вызывается рекурсивно
function countLocalRecursive(uint256 n)
public nonReentrant // use the lock and proceed, revert on next
                    // invocation of nonReentrant guard check
{
    if (n > 0) {
      // do some logic
      count();     

      // make a recursive call, which will trigger
      // the lock check resulting in the transaction
      // being reverted.
      countLocalRecursive(n - 1);
    }
  }

2. Когда функция использует call для вызова функции

function countAndCall(ReentrancyAttack attacker)
public nonReentrant   // use the lock and proceed, revert on next
                      // invocation of nonReentrant guard check
{
    count();

    // Get a hash of the callback function.
    bytes4 func = bytes4(keccak256("callback()"));

    // Trust attacker to do something with a known address.
    // Of course, the attacker will try to exploit our guarded
    // callback in some nefarious scheme and the transaction 
    // will be reverted.
    attacker.callSender(func);
  }

3. Когда внешний контракт снижает ваши функции

function countThisRecursive(uint256 n)
public nonReentrant   // use the lock and proceed, revert on next
                      // invocation of nonReentrant guard check
{
    if (n > 0) {
      count();

      // Use low level call mechanism to make a recursive
      // invocation, which will trigger the lock check 
      // resulting in the transaction being reverted.
      bool result = address(this).call(abi.encodeWithSignature("countThisRecursive(uint256)", n - 1));

      // It is important to `require` this check of the return value
      // otherwise the transaction will continue
      require(result == true);
    }
  }

Детская площадка

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

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

Перед каждым тестом создайте новый целевой экземпляр ReentrancyMock и установите для защищенного ресурса (счетчика) значение 2. Тест будет работать с этим целевым экземпляром.

function beforeEach() public {
        target = new ReentrancyMock();
        // call counter twice to set value to 2
        target.callback();
        target.callback();
    }

Убедитесь, что местные реентерабельные вызовы защищены

Этот тест вызывает функцию countLocalRecursive, которая пытается рекурсивно вызвать себя 3 раза. Он заявляет о сбое, потому что охранник вынуждает вернуться.

function testLocalGuarded() 
public {
    // try to invoke localRecursion that is reentrant.
    bool result = address(target).call(abi.encodeWithSignature("countLocalRecursive(uint256)", 3));
    // it should detect reentrancy and revert
    Assert.isFalse(result, "Guard should prevent reentry");
    // and the counter is not changed
    Assert.equal(target.counter(), 2, "counter should remain 2");
}

Убедитесь, что местные реентерабельные вызовы не охраняются

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

function testLocalVulnerable() 
public {
    // try to invoke localRecursion that is reentrant.
    bool result = address(target).call(abi.encodeWithSignature("countLocalRecursiveVulnerable(uint256)", 3));
    // it does not detect reentrancy
    Assert.isTrue(result, "Does not have a guard");
    // and the counter got fragged
    Assert.equal(target.counter(), 5, "counter got tickled to 5");
}

Заключение

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

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

Возьми код на пробежку! и обязательно зайдите в Open Zeppelin.

дальнейшее чтение

Отказ от ответственности

Я все еще учусь и приветствую комментарии, исправления и улучшения.