Обработка ошибок
Раз уж вчера мы коснулись темы функций - предлагаем продолжить обсуждение далее. Сегодня, мы коснемся одного из самых важных аспектов любой разработки - отлов и обработка возникающих ошибок.
Мы рассмотрим следующие способы работы с ошибками:
Solidity поддерживает 2 сигнатуры ошибок:
Error - используется для "логических" или "пользовательских" ошибок (не приводящих к сбою работы контракта)
Panic - используется для ошибок, при которых контракт не сможет продолжить свою работу, т.е. "критические ошибки"
Assert
Используется для отлова критических ошибок в коде. Проверка осуществляется по логическому условию в аргументе. В зависимости от возвращаемого значения, работа либо продолжится, либо будет выдано исключение. В случае исключения - assert использует весь запас газа, а состояние вернется к исходному (до начала транзакции). Обычно используется для проверки инвариантов и проверки условий внутри контракта. То есть, ошибка, полученная в assert - говорит нам о том, что в коде есть серьезная проблема, которую срочно нужно исправить.
1) Проверка на переполнение (overflow / undflow)
2) Проверка инвариантов (invariants)
3) Проверка состояния (значений полей в памяти) после принятия изменений
4) Предотвращение ситуаций, которые недопустимы
5) Как правило, используется ближе к концу функции
1) Попытка обратиться по отрицательному, или превышающему длину массива индексу
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) )
CharitySplitterFactory.ErrorNotHandled( reason: hex'08c379a00000000000000000000000000000000000000000000000000000 000000000020000000000000000000000000000000000000000000000000000000000000 00116e6f2d6f776e65722d70726f76696465640000000000000000000000000 00000' (type: bytes)
Планируется реализация поддержки кастомных ошибок в будущем, например:
catch CustomErrorA(uint data1) { … } catch CustomErrorB(uint[] memory data2) { … } catch {}
На сегодня на этом остановимся. Если мы что-то упустили, или Вы сможете лучше объяснить ту, или иную тему - будем ждать с нетерпением!