EVM часть 5: передача аргументов в функцию

В предыдущей статье мы разобрались, как устроено хранение mapping’ов в EVM на примере простых типов. Мы научились записывать данные в маппинг и читать.
Однако, в смарт-контрактах, чтобы что-то записать в маппинг или прочитать из маппинга, мы пишем метод. А в метод передаем аргументы. Поэтому нам необходимо узнать, как передавать аргументы в метод.

Предыдущая статья. Следующая статья.

Полный список статей по теме тут.

Заходите в наш телеграмм канал — Blockchain Witnesses! Делитесь опытом или задавайте вопросы, если что-то непонятно.

Передача аргумента в метод

Давайте напишем контракт с методом, который возвращает то, что ему передали. Передавать будем простой тип uint.

Как мы уже делали в предыдущих статьях:

  1. Скомпилируем в Remix и скопируем байткод
  2. Уберем из байткода метаданные, чтобы глаз не мозолили
  3. Декомпилируем
  4. Поправим опкоды, которые не распознал декомпилятор

В итоге получаем байткод:

Из предыдущих статьей мы помним, что контракт содержит конструктор, блоки инициализации, проверки на перечисление wei. Все это мы уже умеем выделять. Нас интересует селектор методов. В нем мы выделяем код прыжка на нашу единственную функцию и таким образом, понимаем где начинается наша функция. А он начинается по адресу — 0x2a. Помним, что после копирования кода в блокчейн, код остается без конструктора, а значит адреса расположения инструкций смещаются. Давайте, поправим адреса, а заодно удалим все до начала нашей функции.

Разберем участок кода до первого прыжка:

Что тут происходит?

Мы помещаем в стэка два значения 0x40 и 0x04 и последнее дублируем. Получаем состояние стэка такое:

0x04
0x04
0x40

Напомню, CALLDATASIZE — помещает в стэк размер сообщения msg.data. Т.е. размер данных, которые были переданы при вызове нашего смарт-контракта. После выполнения команды стэк станет таким:

0x24
0x04
0x04
0x40

Почему именно 0x24 ? Да потому, что мы при вызове нашей функции будут передаваться:

  1. Идентификатор вызываемой функции размером в 4 байта — 0x4a51e107
  2. Аргумент типа unit, размер которого 32 байта или 0x20 в шестнадцатеричном представлении

В итоге 0x04 + 0x20 = 0x24.

Передаваемые в функцию аргументы записываются в msg.data 32 байтовыми слотами сразу после четырехбайтового идентификатора функции.

После этого идет команда SUB — она вычитает из первого аргумента  в стэке второй. Результат помещает на стэк. Зачем это нужно? Таким образом мы получаем длину msg.data без четырехбайтового идентификатора функции. Т.е. длину всех аргументов. Сейчас нам это действие может казаться бессмысленным. Потому что мы с вами знаем длину аргументов. Но EVM, которая будет выполнять наш код заранее размер msg.data не знает! А при вызове смарт-контракта в msg.data могут передавать все что угодно, в том числе некорректные данные.

Итак, после SUB имеем:

0x20
0x04
0x20

Далее идет ряд операций работы со стэком, среди которых одна команда ADD . Пока не очень понятно, что тут происходит. Давайте посмотрим, что будет после прыжка. Стэк после прыжка:

0x04
0x24
0x3c
0x20

Код на который прыгнули:

В этом коде есть интересная инструкция SLT.  Она  проверяет, что первый аргумент на стэке меньше второго. Если это так, то помещает на стэк единицу. Давайте посмотрим стэк до этой инструкции:

0x20
0x20
0x00
0x04
0x24
0x3c
0x20

Очевидно, что инструкция SLT даст нам 0. Это похоже на проверку кол-ва аргументов. И действительно, если бы первый аргумент на стэке был меньше второго, то мы бы прыгнули на 0x5f. А там у нас:

REVERT с возвратом пустого значения. Мы уже знаем, что REVERT в отличие от RETURN‘а, применяется, когда происходит ошибка. Т.е. да скорее всего наше предположении о проверке на кол-во аргументов по размеру верно. Т.е. если размер аргументов меньше 0x20, то мы выходим откатом — REVERT.

Возвращаемся к участку, с которого мы сюда прыгнули, пойдем по другой ветке. Прыгнем на 0xa1. После прыжка стэк будет таким:

0x01
0x00
0x04
0x24
0x3c
0x20

Код после прыжка:

Не похоже на то, что тут происходит что-то интересное, поэтому давайте просчитаем стэк и двинемся дальше. Стэк:

0x00
0x04
0xac
0x00
0x01
0x00
0x04
0x24
0x3c
0x20

Код после прыжка:

А тут уже происходит интересное. А именно, присутствует инструкция CALLDATALOAD . Эта инструкция загружает 32 байта данных из msg.data в стэк. При этом адрес, с которого данные читаются из msg.data указан в стэке. Т.е, вероятно, этот код загружает наш аргумент. Давайте посмотрим на стэк перед вызововом CALLDATALOAD:

0x04
0x00
….

Как видим, на вершине стэка 0x04. А это значит, что CALLDATALOAD загрузит 32 байта из msg.data, которые находятся после 4 байт. Т.е. сразу после идентификатора функции. А сразу после идентификатора функции идет наш аргумент типа uint.

В принципе, дальше код можно не разбирать. Мы получили всю информацию о том, как работает передача аргументов в метод.

Передача аргументов происходит следующим образом:

  1. Получаем длину msg.data инструкцией CALLDATASIZE
  2. Вычисляем длину всех аргументов. Для этого вычитаем из msg.data длины  4 — т.е. размер четырехбайтового идентификатора функции.
  3. Проверяем, что длина всех аргументов не меньше длины аргументов, которые у нас принимает функция.
  4. Загружаем аргументы в стэк начиная с адреса 0x04 инструкцией  CALLDATALOAD

Ну а дальше, работа с полученными аргументами зависит от логики нашего метода. В случае сегодняшнего примера, мы теперь просто можем загрузить аргумент из стэка в память и сделать RETURN.

Резюме

В этой статье мы рассмотрели механизм передачи аргументов в метод. И теперь готовы написать что-то большее чем пустой бесполезный контракт. Чем мы и займемся в следующей статье.

Предыдущая статья. Следующая статья.

Полный список статей по теме тут.

Заходите в наш телеграмм канал — Blockchain Witnesses! Делитесь опытом или задавайте вопросы, если что-то непонятно.

Добавить комментарий