Контракт в Solidity может вызывать другие контракты двумя способами: через интерфейс контракта, что считается высокоуровневым вызовом, или с помощью метода call, что является низкоуровневым подходом.
Несмотря на то, что оба метода используют опкод CALL, Solidity обрабатывает их по-разному.
В этой статье мы сравним их: почему низкоуровневый вызов никогда не возвращается, в то время как высокоуровневый вызов может вернуться, и почему низкоуровневый вызов по пустому адресу считается успешным, а высокоуровневый вызов возвращается при вызове несуществующего контракта.
Почему низкоуровневый вызов (или вызов delegatecall) никогда не возвращается, а вызов через интерфейс контракта может?
Прежде чем объяснить, почему, обратимся к документации Solidity, в которой рассматривается этот вопрос.
Когда исключения происходят в подвызове (sub-call), они «всплывают» - «bubble up» - (т. е. исключения отбрасываются) автоматически, если только они не пойманы в операторе try/catch. Исключением из этого правила являются send и низкоуровневые функции call, delegatecalll и staticcall: они возвращают false в качестве первого возвращаемого значения в случае исключения вместо того, чтобы «bubble up».
Ниже покажу оба вызова, чтобы сравнить их поведение. В примере я использую метод call, но те же принципы можно распространить и на delegatecall.
Вызывающий может вызвать ops() в Called двумя способами. Обратите внимание, что ops() всегда возвращается:
pragma solidity ^0.8.0;
contract Caller {
// first call to ops()
function callByCall(address _address) public returns (bool success) {
(success, ) = _address.call(abi.encodeWithSignature("ops()"));
}
// second call to ops()
function callByInterface(address _address) public {
Called called = Called(_address);
called.ops();
}
}
contract Called {
// ops() always reverts
function ops() public {
revert();
}
}
Несмотря на то, что оба метода используются для вызова одной и той же функции, и оба метода используют опкод CALL, компилятор solidity генерирует байткод для обработки случаев реверта по-разному. Выполнение обеих функций в рамках контракта Caller покажет, что Caller.callByInterface вернется, а Caller.callByCall - нет.
На уровне EVM опкод CALL возвращает булево число, указывающее, был ли вызов успешным или нет, и помещает это возвращение в стек. Сам опкод не вызывает возврата.
Когда вызов выполняется через интерфейс контракта, Solidity обрабатывает это возвращаемое значение за нас. Он явно проверяет, является ли возвращаемое значение ложным, и инициирует возврат, если только вызов не был сделан внутри блока try/catch.
Однако при использовании низкоуровневых вызовов нам нужно вручную обрабатывать возвращаемый булевый символ и при необходимости явно инициировать возврат.
contract Caller {
//...
function callByCall(address address) public returns (bool success) {
(success, ) = address.call(abi.encodeWithSignature("ops()"));
if (!success) {
revert("Something went wront");
}
}
//...
}P.S. Разница между высокоуровневым и низкоуровневым вызовом показана на скрине.
#highlevel #lowlevel #call
