Отвечая на вопрос по следам двух прошлых уроков про байткод: что именно происходит при попытке использования delegatecall? Иными словами, что там в байткоде?
Ответ очень простой: ничего особенного 😄 По факту, это можно проверить самостоятельно с подобными контрактами
contract Target {
uint a;
function callMe(uint _a) external {
a = _a;
}
}
contract Demo {
uint a;
address to;
constructor(address _to) {
to = _to;
}
function doCall() external {
(bool success,) = to.delegatecall(
abi.encodeWithSignature("callMe(uint256)", 42)
);
require(success);
}
}В дебаггере там будет куча не сильно интересных инструкций, которые делают всякие проверки, кодируют сигнатуру и подготавливают стек, но главные инструкции имеют номера 240 и 241 (это с оптимизацией 2000):
GAS(0x5a)
DELEGATECALL (0xf4)
GAS - смотрит, что там по газу осталось, а DELEGATECALL использует значения из стека и определяет с их помощью байткод какого контракта надо выполнить, какой там селектор функции найти и какой аргумент в эту функцию передать. А дальше на DELEGATECALL можно просто нажать кнопку step into и guess what: вы прыгнете к байткоду другого контракта, где все инструкции начинаются опять с нуля, причём в начале будет уже знакомый набор
000 PUSH1 80 - LINE 5
002 PUSH1 40 - LINE 5
004 MSTORE - LINE 5
То есть пока крутится delegatecall, у него там своя атмосфера и своя память. В calldata же будет содержаться что-то такое
0xe73620c3000000000000000000000000000000000000000000000000000000000000002a
Это селектор функции callMe и 0x2a, что равно значению 42 в десятичном формате. То есть я просто в
a хочу положить 42. Этот код выполнится в контексте нашего же контракта, но вот память на момент вызова окажется как бы новой.
Почему это можно делать? Потому что *читать* байткод чужого контракта нам никто не мешает, ведь в блокчейне всё публично. Значит мы в принципе можем взять код и его исполнить у себя. Что делать нельзя, так это напрямую что-то менять в чужом состоянии, забирать у кого-то деньги или вносить в байткод изменения, ясное дело.
Ну, а дальше опкоды будут очень похожи на то, что мы видели. Например, мы увидим
026 CALLDATALOAD - LINE 5
027 PUSH1 e0 - LINE 5
029 SHR - LINE 5
Опять как в уроке - он читает calldata и вычленяет оттуда селектор.
А потом
031 PUSH4 e73620c3 - LINE 5
036 EQ - LINE 5
Ну то есть сравниваем селектор. А дальше там всё просто - он берёт из calldata аргумент и говорит
057 PUSH1 00 -
059 SSTORE -
0 - это номер слота переменной
a в контракте
Target, тк она там на самой первой позиции (потому что в момент компиляции контракта Target он знает, какой слот был у
a). Но тк это выполняется в нашем контексте, то по факту *мы имеем ввиду нулевой слот в Demo*. Именно поэтому переменные должны быть перечислены в правильном порядке.
Последняя инструкция в рамках delegatecall:
062 STOP - LINE 9
Дальше он возвращается к исходному байткоду, там меряет returndatasize (чтобы понять вернула ли что-нибудь функция), проверяет require и просто говорит STOP, то есть транзакция завершается.
Иными словами, никакой магии особо нет: он прямо берёт чужой байткод и начинает его прогонять с самого начала, но только для своего контекста.