В предыдущей статье мы разобрались, как устроено хранение mapping’ов в EVM на примере простых типов. Мы научились записывать данные в маппинг и читать.
Однако, в смарт-контрактах, чтобы что-то записать в маппинг или прочитать из маппинга, мы пишем метод. А в метод передаем аргументы. Поэтому нам необходимо узнать, как передавать аргументы в метод.
Предыдущая статья. Следующая статья.
Полный список статей по теме тут.
Заходите в наш телеграмм канал — Blockchain Witnesses! Делитесь опытом или задавайте вопросы, если что-то непонятно.
Передача аргумента в метод
Давайте напишем контракт с методом, который возвращает то, что ему передали. Передавать будем простой тип uint.
1 2 3 4 5 6 7 8 9 10 11 |
// SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.25; contract BlockwitEmptyContract { function pingPong(uint value) pure public returns( uint) { return value; } } |
Как мы уже делали в предыдущих статьях:
- Скомпилируем в Remix и скопируем байткод
- Уберем из байткода метаданные, чтобы глаз не мозолили
- Декомпилируем
- Поправим опкоды, которые не распознал декомпилятор
В итоге получаем байткод:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 |
// Конструктор label_0000: 0000 60 PUSH1 0x80 0002 60 PUSH1 0x40 0004 52 MSTORE 0005 34 CALLVALUE 0006 80 DUP1 0007 15 ISZERO 0008 60 PUSH1 0x0e 000A 57 JUMPI 000B 5F PUSH0 000C 80 DUP1 000D FD REVERT label_000E: 000E 5B JUMPDEST 000F 50 POP 0010 61 PUSH2 0x010f 0013 80 DUP1 0014 61 PUSH2 0x001c 0017 5F PUSH0 0018 39 CODECOPY 0019 5F PUSH0 001A F3 RETURN 001B FE ASSERT // Эффективный код смарт-контракта 001C 60 PUSH1 0x80 001E 60 PUSH1 0x40 0020 52 MSTORE 0021 34 CALLVALUE 0022 80 DUP1 0023 15 ISZERO 0024 60 PUSH1 0x0e 0026 57 JUMPI 0027 5F 5F 0028 80 DUP1 0029 FD REVERT // Селектор методов 002A 5B JUMPDEST 002B 50 POP 002C 60 PUSH1 0x04 002E 36 CALLDATASIZE 002F 10 LT 0030 60 PUSH1 0x26 0032 57 *JUMPI 0033 5F 5F 0034 35 CALLDATALOAD 0035 60 PUSH1 0xe0 0037 1C SHR 0038 80 DUP1 0039 63 PUSH4 0x4a51e107 // хэш нашей функции 003E 14 EQ 003F 60 PUSH1 0x2a 0041 57 JUMPI // переход на 0x2a, 0x2a + 0x1c (размер конструктора) = 0x46 0042 5B JUMPDEST 0043 5F 5F 0044 80 DUP1 0045 FD REVERT // Код нашей функции 0046 5B JUMPDEST 0047 60 PUSH1 0x40 0049 60 PUSH1 0x04 004B 80 DUP1 004C 36 CALLDATASIZE 004D 03 SUB 004E 81 DUP2 004F 01 ADD 0050 90 SWAP1 0051 60 PUSH1 0x3c 0053 91 SWAP2 0054 90 SWAP1 0055 60 PUSH1 0x8f 0057 56 JUMP 0058 5B JUMPDEST 0059 60 PUSH1 0x54 005B 56 JUMP 005C 5B JUMPDEST 005D 60 PUSH1 0x40 005F 51 MLOAD 0060 60 PUSH1 0x4b 0062 91 SWAP2 0063 90 SWAP1 0064 60 PUSH1 0xc2 0066 56 *JUMP 0067 5B JUMPDEST 0068 60 PUSH1 0x40 006A 51 MLOAD 006B 80 DUP1 006C 91 SWAP2 006D 03 SUB 006E 90 SWAP1 006F F3 *RETURN 0070 5B JUMPDEST 0071 5F 5F 0072 81 DUP2 0073 90 SWAP1 0074 50 POP 0075 91 SWAP2 0076 90 SWAP1 0077 50 POP 0078 56 JUMP 0079 5B JUMPDEST 007A 5F 5F 007B 80 DUP1 007C FD *REVERT 007D 5B JUMPDEST 007E 5F 5F 007F 81 DUP2 0080 90 SWAP1 0081 50 POP 0082 91 SWAP2 0083 90 SWAP1 0084 50 POP 0085 56 *JUMP 0086 5B JUMPDEST 0087 60 PUSH1 0x71 0089 81 DUP2 008A 60 PUSH1 0x61 008C 56 *JUMP 008D 5B JUMPDEST 008E 81 DUP2 008F 14 EQ 0090 60 PUSH1 0x7a 0092 57 JUMPI 0093 5F PUSH0 0094 80 DUP1 0095 FD REVERT 0096 5B JUMPDEST 0097 50 POP 0098 56 JUMP 0099 5B JUMPDEST 009A 5F PUSH0 009B 81 DUP2 009C 35 CALLDATALOAD 009D 90 SWAP1 009E 50 POP 009F 60 PUSH1 0x89 00A1 81 DUP2 00A2 60 PUSH1 0x6a 00A4 56 JUMP 00A5 5B JUMPDEST 00A6 92 SWAP3 00A7 91 SWAP2 00A8 50 POP 00A9 50 POP 00AA 56 JUMP 00AB 5B JUMPDEST 00AC 5F PUSH0 00AD 60 PUSH1 0x20 00AF 82 DUP3 00B0 84 DUP5 00B1 03 SUB 00B2 12 SLT 00B3 15 ISZERO 00B4 60 PUSH1 0xa1 00B6 57 JUMPI 00B7 60 PUSH1 0xa0 00B9 60 PUSH1 0x5d 00BB 56 JUMP 00BC 5B JUMPDEST 00BD 5B JUMPDEST 00BE 5F PUSH0 00BF 60 PUSH1 0xac 00C1 84 DUP5 00C2 82 DUP3 00C3 85 DUP6 00C4 01 ADD 00C5 60 PUSH1 0x7d 00C7 56 JUMP 00C8 5B JUMPDEST 00C9 91 SWAP2 00CA 50 POP 00CB 50 POP 00CC 92 SWAP3 00CD 91 SWAP2 00CE 50 POP 00CF 50 POP 00D0 56 JUMP 00D1 5B JUMPDEST 00D2 60 PUSH1 0xbc 00D4 81 DUP2 00D5 60 PUSH1 0x61 00D7 56 JUMP 00D8 5B JUMPDEST 00D9 82 DUP3 00DA 52 MSTORE 00DB 50 POP 00DC 50 POP 00DD 56 JUMP 00DE 5B JUMPDEST 00DF 5F PUSH0 00E0 60 PUSH1 0x20 00E2 82 DUP3 00E3 01 ADD 00E4 90 SWAP1 00E5 50 POP 00E6 60 PUSH1 0xd3 00E8 5F PUSH0 00E9 83 DUP4 00EA 01 ADD 00EB 84 DUP5 00EC 60 PUSH1 0xb5 00EE 56 JUMP 00EF 5B JUMPDEST 00F0 92 SWAP3 00F1 91 SWAP2 00F2 50 POP 00F3 50 POP 00F4 56 JUMP 00F5 FE ASSERT |
Из предыдущих статьей мы помним, что контракт содержит конструктор, блоки инициализации, проверки на перечисление wei. Все это мы уже умеем выделять. Нас интересует селектор методов. В нем мы выделяем код прыжка на нашу единственную функцию и таким образом, понимаем где начинается наша функция. А он начинается по адресу — 0x2a. Помним, что после копирования кода в блокчейн, код остается без конструктора, а значит адреса расположения инструкций смещаются. Давайте, поправим адреса, а заодно удалим все до начала нашей функции.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 |
002A 5B JUMPDEST 002B 60 PUSH1 0x40 002D 60 PUSH1 0x04 002F 80 DUP1 0030 36 CALLDATASIZE 0031 03 SUB 0032 81 DUP2 0033 01 ADD 0034 90 SWAP1 0035 60 PUSH1 0x3c 0037 91 SWAP2 0038 90 SWAP1 0039 60 PUSH1 0x8f 003B 56 JUMP 003C 5B JUMPDEST 003D 60 PUSH1 0x54 003F 56 JUMP 0040 5B JUMPDEST 0041 60 PUSH1 0x40 0043 51 MLOAD 0044 60 PUSH1 0x4b 0046 91 SWAP2 0047 90 SWAP1 0048 60 PUSH1 0xc2 004A 56 JUMP 004B 5B JUMPDEST 004C 60 PUSH1 0x40 004E 51 MLOAD 004F 80 DUP1 0050 91 SWAP2 0051 03 SUB 0052 90 SWAP1 0053 F3 *RETURN 0054 5B JUMPDEST 0055 5F 5F 0056 81 DUP2 0057 90 SWAP1 0058 50 POP 0059 91 SWAP2 005A 90 SWAP1 005B 50 POP 005C 56 JUMP 005D 5B JUMPDEST 005E 5F PUSH0 005F 80 DUP1 0060 FD REVERT 0061 5B JUMPDEST 0062 5F PUSH0 0063 81 DUP2 0064 90 SWAP1 0065 50 POP 0066 91 SWAP2 0067 90 SWAP1 0068 50 POP 0069 56 JUMP 006A 5B JUMPDEST 006B 60 PUSH1 0x71 006D 81 DUP2 006E 60 PUSH1 0x61 0070 56 JUMP 0071 5B JUMPDEST 0072 81 DUP2 0073 14 EQ 0074 60 PUSH1 0x7a 0076 57 JUMPI 0077 5F 5F 0078 80 DUP1 0079 FD REVERT 007A 5B JUMPDEST 007B 50 POP 007C 56 JUMP 007D 5B JUMPDEST 007E 5F PUSH0 007F 81 DUP2 0080 35 CALLDATALOAD 0081 90 SWAP1 0082 50 POP 0083 60 PUSH1 0x89 0085 81 DUP2 0086 60 PUSH1 0x6a 0088 56 JUMP 0089 5B JUMPDEST 008A 92 SWAP3 008B 91 SWAP2 008C 50 POP 008D 50 POP 008E 56 JUMP 008F 5B JUMPDEST 0090 5F PUSH0 0091 60 PUSH1 0x20 0093 82 DUP3 0094 84 DUP5 0095 03 SUB 0096 12 SLT 0097 15 ISZERO 0098 60 PUSH1 0xa1 009A 57 JUMPI 009B 60 PUSH1 0xa0 009D 60 PUSH1 0x5d 009F 56 JUMP 00A0 5B JUMPDEST 00A1 5B JUMPDEST 00A2 5F PUSH0 00A3 60 PUSH1 0xac 00A5 84 DUP5 00A6 82 DUP3 00A7 85 DUP6 00A8 01 ADD 00A9 60 PUSH1 0x7d 00AB 56 JUMP 00AC 5B JUMPDEST 00AD 91 SWAP2 00AE 50 POP 00AF 50 POP 00B0 92 SWAP3 00B1 91 SWAP2 00B2 50 POP 00B3 50 POP 00B4 56 JUMP 00B5 5B JUMPDEST 00B6 60 PUSH1 0xbc 00B8 81 DUP2 00B9 60 PUSH1 0x61 00BB 56 JUMP 00BC 5B JUMPDEST 00BD 82 DUP3 00BE 52 MSTORE 00BF 50 POP 00C0 50 POP 00C1 56 JUMP 00C2 5B JUMPDEST 00C3 5F PUSH0 00C4 60 PUSH1 0x20 00C6 82 DUP3 00C7 01 ADD 00C8 90 SWAP1 00C9 50 POP 00CA 60 PUSH1 0xd3 00CC 5F PUSH0 00CD 83 DUP4 00CE 01 ADD 00CF 84 DUP5 00D0 60 PUSH1 0xb5 00D2 56 JUMP 00D3 5B JUMPDEST 00D4 92 SWAP3 00D5 91 SWAP2 00D6 50 POP 00D7 50 POP 00D8 56 JUMP 00D9 FE ASSERT |
Разберем участок кода до первого прыжка:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
002A 5B JUMPDEST 002B 60 PUSH1 0x40 002D 60 PUSH1 0x04 002F 80 DUP1 0030 36 CALLDATASIZE 0031 03 SUB 0032 81 DUP2 0033 01 ADD 0034 90 SWAP1 0035 60 PUSH1 0x3c 0037 91 SWAP2 0038 90 SWAP1 0039 60 PUSH1 0x8f 003B 56 JUMP |
Что тут происходит?
Мы помещаем в стэка два значения 0x40 и 0x04 и последнее дублируем. Получаем состояние стэка такое:
0x04 |
0x04 |
0x40 |
Напомню, CALLDATASIZE — помещает в стэк размер сообщения msg.data. Т.е. размер данных, которые были переданы при вызове нашего смарт-контракта. После выполнения команды стэк станет таким:
0x24 |
0x04 |
0x04 |
0x40 |
Почему именно 0x24 ? Да потому, что мы при вызове нашей функции будут передаваться:
- Идентификатор вызываемой функции размером в 4 байта — 0x4a51e107
- Аргумент типа 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 |
Код на который прыгнули:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
008F 5B JUMPDEST 0090 5F PUSH0 0091 60 PUSH1 0x20 0093 82 DUP3 0094 84 DUP5 0095 03 SUB 0096 12 SLT 0097 15 ISZERO 0098 60 PUSH1 0xa1 009A 57 JUMPI 009B 60 PUSH1 0xa0 009D 60 PUSH1 0x5d 009F 56 JUMP |
В этом коде есть интересная инструкция SLT. Она проверяет, что первый аргумент на стэке меньше второго. Если это так, то помещает на стэк единицу. Давайте посмотрим стэк до этой инструкции:
0x20 |
0x20 |
0x00 |
0x04 |
0x24 |
0x3c |
0x20 |
Очевидно, что инструкция SLT даст нам 0. Это похоже на проверку кол-ва аргументов. И действительно, если бы первый аргумент на стэке был меньше второго, то мы бы прыгнули на 0x5f. А там у нас:
1 2 3 4 |
005D 5B JUMPDEST 005E 5F PUSH0 005F 80 DUP1 0060 FD REVERT |
REVERT с возвратом пустого значения. Мы уже знаем, что REVERT в отличие от RETURN‘а, применяется, когда происходит ошибка. Т.е. да скорее всего наше предположении о проверке на кол-во аргументов по размеру верно. Т.е. если размер аргументов меньше 0x20, то мы выходим откатом — REVERT.
Возвращаемся к участку, с которого мы сюда прыгнули, пойдем по другой ветке. Прыгнем на 0xa1. После прыжка стэк будет таким:
0x01 |
0x00 |
0x04 |
0x24 |
0x3c |
0x20 |
Код после прыжка:
1 2 3 4 5 6 7 8 9 |
00A1 5B JUMPDEST 00A2 5F PUSH0 00A3 60 PUSH1 0xac 00A5 84 DUP5 00A6 82 DUP3 00A7 85 DUP6 00A8 01 ADD 00A9 60 PUSH1 0x7d 00AB 56 JUMP |
Не похоже на то, что тут происходит что-то интересное, поэтому давайте просчитаем стэк и двинемся дальше. Стэк:
0x00 |
0x04 |
0xac |
0x00 |
0x01 |
0x00 |
0x04 |
0x24 |
0x3c |
0x20 |
Код после прыжка:
1 2 3 4 5 6 7 8 9 10 |
007D 5B JUMPDEST 007E 5F PUSH0 007F 81 DUP2 0080 35 CALLDATALOAD 0081 90 SWAP1 0082 50 POP 0083 60 PUSH1 0x89 0085 81 DUP2 0086 60 PUSH1 0x6a 0088 56 JUMP |
А тут уже происходит интересное. А именно, присутствует инструкция CALLDATALOAD . Эта инструкция загружает 32 байта данных из msg.data в стэк. При этом адрес, с которого данные читаются из msg.data указан в стэке. Т.е, вероятно, этот код загружает наш аргумент. Давайте посмотрим на стэк перед вызововом CALLDATALOAD:
0x04 |
0x00 |
…. |
Как видим, на вершине стэка 0x04. А это значит, что CALLDATALOAD загрузит 32 байта из msg.data, которые находятся после 4 байт. Т.е. сразу после идентификатора функции. А сразу после идентификатора функции идет наш аргумент типа uint.
В принципе, дальше код можно не разбирать. Мы получили всю информацию о том, как работает передача аргументов в метод.
Передача аргументов происходит следующим образом:
- Получаем длину msg.data инструкцией CALLDATASIZE
- Вычисляем длину всех аргументов. Для этого вычитаем из msg.data длины 4 — т.е. размер четырехбайтового идентификатора функции.
- Проверяем, что длина всех аргументов не меньше длины аргументов, которые у нас принимает функция.
- Загружаем аргументы в стэк начиная с адреса 0x04 инструкцией CALLDATALOAD
Ну а дальше, работа с полученными аргументами зависит от логики нашего метода. В случае сегодняшнего примера, мы теперь просто можем загрузить аргумент из стэка в память и сделать RETURN.
Резюме
В этой статье мы рассмотрели механизм передачи аргументов в метод. И теперь готовы написать что-то большее чем пустой бесполезный контракт. Чем мы и займемся в следующей статье.
Предыдущая статья. Следующая статья.
Полный список статей по теме тут.
Заходите в наш телеграмм канал — Blockchain Witnesses! Делитесь опытом или задавайте вопросы, если что-то непонятно.