February 18, 2022

Обработка ошибок

Друзья, привет!

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

Мы рассмотрим следующие способы работы с ошибками:

  • assert
  • require
  • revert
  • try/catch

Solidity поддерживает 2 сигнатуры ошибок:

  • Error (string)
  • Panic (uint256)

Error - используется для "логических" или "пользовательских" ошибок (не приводящих к сбою работы контракта)

Panic - используется для ошибок, при которых контракт не сможет продолжить свою работу, т.е. "критические ошибки"

Assert

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

Область использования:

1) Проверка на переполнение (overflow / undflow)

2) Проверка инвариантов (invariants)

3) Проверка состояния (значений полей в памяти) после принятия изменений

4) Предотвращение ситуаций, которые недопустимы

5) Как правило, используется ближе к концу функции

Примеры возникновения:

1) Попытка обратиться по отрицательному, или превышающему длину массива индексу

2) Деление на 0

3) Попытка вызова непроинициализированной переменной

4) Попытка вызова pop() у пустого массива

5) Вызов assert с условием == false

Использует 0xfe opcode для инициирования ошибки

Необходимо отметить, что условие внутри assert, обратно условию в if:

if (msg.sender != manager) 
 // but 
assert(msg.sender == manager)

Пример:

contract Assert { 
  // Adds two numbers 
  function add(uint256 a, uint256 b) internal pure returns (uint256) { 
    uint256 c = a + b;  
    // assert использутеся для проверки системных ошибок 
    // сработает при переполнении
    assert(c >= a);  
    return c; 
  }
}

Require

В отличии от assert весь неиспользованный газ возвращается обратно. Является наиболее часто используемой функции в Solidity для отладки и отлова ошибок. Обычно используется для проверки возвращаемых значений из вызовов "внешних" контрактов или для того, чтобы гарантировать корректные условия выполнения кода.

Интерфейс функции Require выглядит следующим образом:

require(<condition>) - без удобочитаемого текста ошибки

require(<condition>, [errorText]) - с текстом ошибки

Использует 0xfd opcode для инициирования ошибки

require(msg.sender == manager)
require(msg.sender == manager, "should be manager")

Область использования:

1) Проверка подаваемых пользователем значений [ require(input < 20) ]

2) Проверка результата вызова функции во внешнем контракте

3) Проверка состояния перед началом выполнением операции

4) Как правило, используется в начале функции

Revert

Аналогично require, возвращает неиспользованный газ и позволяет вернуть текст ошибки.

Область использования:

Очень близка к require, но обычно используется для более сложных условий (синтаксически удобнее).

Интерфейс:

revert() - без удобочитаемого текста ошибки

revert(<textError>) - с текстом ошибки

contract Purchase { 
  uint price = 2 ether;  
  function purchaseItem(uint _amount) public payable { 
    if (msg.value != _amount * price) { 
      revert("Only exact payments!"); 
    }  
    
    // Альтернативный вариант с require
    require(msg.value == _amount * price, "Only exact payments!"); 
  }
}

Для revert вы можете использовать и собственные обработчики ошибок, например

// custom error 
error InsufficientBalance(uint balance, uint withdrawAmount); 
function testCustomError(uint _withdrawAmount) public view { 
  uint bal = address(this).balance; 
  if (bal < _withdrawAmount) { 
    revert InsufficientBalance({balance: bal, 
                                withdrawAmount: _withdrawAmount}); 
  } 
}

Try-catch

Конструкция try-catch в solidity позволяет Вам реагировать на ошибки при внешних вызовах или создании экземпляра стороннего контракта.

Пример:

pragma solidity ^0.6.1;

contract CharitySplitter {
    address public owner;
    constructor (address _owner) public {
        require(_owner != address(0), "no-owner-provided");
        owner = _owner;
    }
}
pragma solidity ^0.6.1;

import "./CharitySplitter.sol";

contract CharitySplitterFactory {
    mapping (address => CharitySplitter) public charitySplitters;
    uint public errorCount;
    event ErrorHandled(string reason);
    event ErrorNotHandled(bytes reason);
    function createCharitySplitter(address charityOwner) public {
        try new CharitySplitter(charityOwner)
            returns (CharitySplitter newCharitySplitter)
        {
            charitySplitters[msg.sender] = newCharitySplitter;
        } catch {
            errorCount++;
        }
    }
}

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

Например:

function createCharitySplitter(address _charityOwner) public {
    try new CharitySplitter(getCharityOwner(_charityOwner, false))
        returns (CharitySplitter newCharitySplitter)
    {
        charitySplitters[msg.sender] = newCharitySplitter;
    } catch (bytes memory reason) {
        ...
    }
}
function getCharityOwner(address _charityOwner, bool _toPass)
        internal returns (address) {
    require(_toPass, "revert-required-for-testing");
    return _charityOwner;
}

Здесь в конструктор подается не конкретное значение, а вычисляемое значение через функцию getCharityOwner. В случае, если отработает require в этой функции - он не будет перехвачен блоком catch.

Можно дополнительно расширить возможности перехвата ошибки в блоке catch, путем указания 2 типов ошибки:

catch Error (string memory reason)

Результат:

CharitySplitterFactory.ErrorHandled(
    reason: 'no-owner-provided' (type: string)
)

catch (bytes memory reason)

Результат:

CharitySplitterFactory.ErrorNotHandled(
  reason: hex'08c379a00000000000000000000000000000000000000000000000000000
  000000000020000000000000000000000000000000000000000000000000000000000000
  00116e6f2d6f776e65722d70726f76696465640000000000000000000000000
  00000' (type: bytes)
  

Планируется реализация поддержки кастомных ошибок в будущем, например:

catch CustomErrorA(uint data1) { … }
catch CustomErrorB(uint[] memory data2) { … }
catch {}

На сегодня на этом остановимся. Если мы что-то упустили, или Вы сможете лучше объяснить ту, или иную тему - будем ждать с нетерпением!

Список источников:

  1. Один
  2. Два
  3. Три
  4. Четыре