О, сегодня будет один интересный пункт, который порой упускают из виду даже опытные разработчики, и связан он с темой низкоуровневого вызова call.
8. call bypasses function existence check, type checking and argument packing
Вызов call пропускает проверку существования функции, проверку типа данных и упаковку аргументов. Что это означает?
Вообще, call - это такая "зараза", которую нужно контролировать вдоль и поперек. Уже не первый десяток уязвимостей было найдено, только потому что разработчики забыли сделать какую-либо проверку после вызова этого опкода.
Отправь его и он уйдет без вопросов...
Даже в самой документации есть специальная сноска по этому поводу:
The low-level functions call, delegatecall and staticcall return true as their first return value if the account called is non-existent, as part of the design of the EVM. Account existence must be checked prior to calling if needed.
Грубо говоря, что низкоуровневые языки всегда будут возвращать true как первое возвратное значение, даже если аккаунт, на который идет вызов, не существует. Так заложено в EVM и проверка на существование аккаунта ложится на плечи разработчика.
Также и с функциями. Посмотрите на два контракта:
contract Caller {
function testCallFoo(address payable _addr) public payable {
(bool success, bytes memory data) = _addr.call{
value: msg.value,
gas: 5000
}(abi.encodeWithSignature("foo(string,uint256)", "call foo", 123));
}
function testCallDoesNotExist(address payable _addr) public payable {
(bool success, bytes memory data) = _addr.call{value: msg.value}(
abi.encodeWithSignature("doesNotExist()")
);
}
}
contract Receiver {
event Received(address caller, uint256 amount, string message);
fallback() external payable {
emit Received(msg.sender, msg.value, "Fallback was called");
}
function foo(string memory _message, uint256 _x)
public
payable
returns (uint256)
{
emit Received(msg.sender, msg.value, _message);
return _x + 1;
}
}
Из контракта caller мы делаем вызовы во второй контракт. И что самое удивительное, обе функции сработают с call, учитывая то, что в receiver нет функции doesNotExist(), которую мы пытаемся вызвать через testCallDoesNotExist().
А если убрать fallback(), куда падает вызов несуществующих функций, наш call все равно не откатится.
Более того, call все равно как вы упаковываете типы данных. Это также проблема разработчиков правильно передавать данные в другой контракт или функцию.
Кстати, для этих целей есть abi.encodeCall - который практически тоже самое, что и abi.encode, но только добавляет проверку типов данных на соответствие функции, которая была указана в аргументах. Результат кодирования схож с abi.encodeWithSelector.
Подводя итоги можно сказать сказать общепринятые правила в использовании call:
1. Всегда проверять успешность возвращаемого значения;
2. Знать наверняка или самому контролировать вопрос существования адреса, куда отправляется вызов;
3. Проверять правильность кодирования данных при отправке;
Это если и не избавит вас на 100% от ошибок и проблем с call вызовов, но значительно сократить их шанс.
Будьте аккуратны с низкоуровневыми вызовами!
#call