Идем дальше, и на очереди у нас пункт под номером пять:
5. Before using delegatecall, ensure that storage layout is in same order in both contracts
Используя функцию delegatecall(), убедитесь, что переменные состояния идут в одинаковом порядке.
Еще одна популярная проблема с низкоуровневыми функциями. Возьмем к примеру два контракта:
contract A {
address public sender;
address public owner;
function setAddress(address _contract, address addr) public payable {
(bool success, bytes memory data) = _contract.delegatecall(
abi.encodeWithSignature("setAddress(address)", addr)
);
}
}
contract B {
address public owner;
address public sender;
function setAddress(address addr) public payable {
owner = msg.sender;
}
}
Если вы не помните основную особенность delegatecall(), то напомню, что он "как бы забирает" функцию из другого контракта и выполняет ее в рамках контракта, где он был вызван.
Если мы вызовем функцию setAddress() в контракте А, то и изменение в памяти произойдет в контракте А, а не в контракте В, куда идет вызов.
В данном примере мы видим, что функция setAddress() в контракте В должна обновить переменную owner. Допустим, мы хотим обновить такую же переменную у себя в контракте А.
Сделав вызов функции А::setAddress(), транзакция пройдет успешно, но в А обновится переменная sender, а не owner, как это было запланировано!
Это произошло именно из-за того, что был нарушен порядок переменных в контракте. Поэтому за ним нужно пристально следить. Да и вообще, если не знаете точно "зачем", то использовать delegatecall крайне не безопасно.
Если хотите сами попробовать разобраться в этой проблеме, то рекомендую пройти задачу Ethernaut 16, прекрасный пример, когда низкоуровневая функция может принести проблемы. Да и сама задача приближена к реальному контракту.
#delegatecall